安卓UVC控制协议入门

最近的项目里面需要对UVC摄像头进行操控,简单的了解了下相关的知识。

首先UVC全称为USB video(device) class,是微软与另外几家设备厂商联合推出的为USB视频捕获设备定义的协议标准,目前已成为USB org标准之一。在USB中文网上其实有比较详细的描述,但是新手直接上来就看这个协议其实是比较懵逼的。所以可以参考UVCCamera这个安卓项目的源码去辅助理解。

USB描述符

当我们连接到一个UVC设备之后其实第一步应该先获取它的描述符来看看它具体支持哪些操作。UVCCamera里是使用libusb获取到USB的设备描述符之后在uvc_scan_control里面解析的。实际上我们也可以利用安卓应用层的UsbDeviceConnection.getRawDescriptors接口获取到USB的描述符然后再java层解析:

val manager = context.getSystemService(AppCompatActivity.USB_SERVICE) as UsbManager
val deviceIterator: Iterator<UsbDevice> = manager.deviceList.values.iterator()
while (deviceIterator.hasNext()) {
    val device = deviceIterator.next()
    if (device.vendorId == targetVid && device.productId == targetPid) {
        val connect = manager.openDevice(device)
        val desc = connect.rawDescriptors
        // 解析usb描述符
        connect.close()
        return
    }
}

这里拿到的是一个byte数组,我们先要理解什么是usb描述符才能去解析它。usb描述符其实就是描述usb的属性和用途的,四种主要描述符的逻辑结构大概如下:

1.png
  • 设备描述符: 每一个USB设备只有一个设备描述符,主要向主机说明设备类型、端点0最大包长、设备版本、配置数量等等
  • 配置描述符: 每一个USB设备至少有一个或者多个配置描述符,但是主机同一时间只能选择某一种配置,标准配置描述符主要向主机描述当前配置下的设备属性、所需电流、支持的接口数、配置描述符集合长度等等。
  • 接口描述符 : 每一个USB配置下至少有一个或者多个接口描述符,接口描述符主要说明设备类型、此接口下使用的端点数(不包括0号端点),一个接口就是实现一种功能,实现这种功能可能需要端点0就够了,可能还需要其它的端点配合。
  • 端点描述符: 每一个USB接口下可以有多个端点描述符,端点描述符用来描述符端点的各种属性。

端点是实现USB设备功能的物理缓冲区实体,USB主机和设备是通过端点进行数据交互的

描述符解析

所有类型的描述符的前两个字节定义都是一样的,第一个字节指定描述符的长度,而第二个字节表示描述符类型。所以我们拿到rawDescriptors之后可以用下面的代码去遍历解析:

var index = 0
val descriptorTypes = mapOf(
    0x01.toByte() to "DEVICE",
    0x02.toByte() to "CONFIG",
    0x04.toByte() to "INTERFACE",
    0x05.toByte() to "ENDPOINT",
)

while (index < desc.size) {
    descriptorTypes[desc[index + 1]]?.let {
        val indent = " ".repeat(desc[index + 1].toInt())
        Log.d(TAG, "${indent}$it")
    }
    index += desc[index]
}

我这个调试设备的描述符解析如下:

 DEVICE
  CONFIG
    INTERFACE
     ENDPOINT
    INTERFACE
     ENDPOINT
    INTERFACE
     ENDPOINT
     ENDPOINT

可以看到它有一个设备描述符,这个设备描述符下有个一个配置描述符,这个配置描述符下有三个接口描述符,每个接口描述符下又有一到两个端点描述符。

知道了描述符的类型就能在USB标准里找到它具体的数据结构去解析,例如设备描述符的定义如下:

struct libusb_device_descriptor {
    /** Size of this descriptor (in bytes) */
    uint8_t  bLength;

    /** Descriptor type. Will have value
     * \ref libusb_descriptor_type::LIBUSB_DT_DEVICE LIBUSB_DT_DEVICE in this
     * context. */
    uint8_t  bDescriptorType;

    /** USB specification release number in binary-coded decimal. A value of
     * 0x0200 indicates USB 2.0, 0x0110 indicates USB 1.1, etc. */
    uint16_t bcdUSB;

    /** USB-IF class code for the device. See \ref libusb_class_code. */
    uint8_t  bDeviceClass;

    /** USB-IF subclass code for the device, qualified by the bDeviceClass
     * value */
    uint8_t  bDeviceSubClass;

    /** USB-IF protocol code for the device, qualified by the bDeviceClass and
     * bDeviceSubClass values */
    uint8_t  bDeviceProtocol;

    /** Maximum packet size for endpoint 0 */
    uint8_t  bMaxPacketSize0;

    /** USB-IF vendor ID */
    uint16_t idVendor;

    /** USB-IF product ID */
    uint16_t idProduct;

    /** Device release number in binary-coded decimal */
    uint16_t bcdDevice;

    /** Index of string descriptor describing manufacturer */
    uint8_t  iManufacturer;

    /** Index of string descriptor describing product */
    uint8_t  iProduct;

    /** Index of string descriptor containing device serial number */
    uint8_t  iSerialNumber;

    /** Number of possible configurations */
    uint8_t  bNumConfigurations;
};

描述符类型定义的id可以参考lsusb的libusb_descriptor_type枚举:

enum libusb_descriptor_type {
    /** Device descriptor. See libusb_device_descriptor. */
    LIBUSB_DT_DEVICE = 0x01,

    /** Configuration descriptor. See libusb_config_descriptor. */
    LIBUSB_DT_CONFIG = 0x02,

    /** String descriptor */
    LIBUSB_DT_STRING = 0x03,

    /** Interface descriptor. See libusb_interface_descriptor. */
    LIBUSB_DT_INTERFACE = 0x04,

    /** Endpoint descriptor. See libusb_endpoint_descriptor. */
    LIBUSB_DT_ENDPOINT = 0x05,

    /** Interface Association Descriptor.
    * See libusb_interface_association_descriptor */
    LIBUSB_DT_INTERFACE_ASSOCIATION = 0x0b,

    /** BOS descriptor */
    LIBUSB_DT_BOS = 0x0f,

    /** Device Capability descriptor */
    LIBUSB_DT_DEVICE_CAPABILITY = 0x10,

    /** HID descriptor */
    LIBUSB_DT_HID = 0x21,

    /** HID report descriptor */
    LIBUSB_DT_REPORT = 0x22,

    /** Physical descriptor */
    LIBUSB_DT_PHYSICAL = 0x23,

    /** Hub descriptor */
    LIBUSB_DT_HUB = 0x29,

    /** SuperSpeed Hub descriptor */
    LIBUSB_DT_SUPERSPEED_HUB = 0x2a,

    /** SuperSpeed Endpoint Companion descriptor */
    LIBUSB_DT_SS_ENDPOINT_COMPANION = 0x30
};

可以看到描述符的类型其实不止上面四种,还有很多其他的类型。例如我就能从UVC 相机终端描述符里面的bmControls字段解析出相机具体支持的操作:

mControls:使用位图来表示支持的视频流。

  • D0:扫描模式 //扫描模式(逐行扫描或隔行扫描)
  • D1:自动曝光模式
  • D2:自动曝光优先级
  • D3:曝光时间(绝对值)
  • D4:曝光时间(相对)
  • D5:焦点(绝对)
  • D6:焦点(相对)
  • ...

libusb里面这个UVC相机终端描述符会作为接口描述符的拓展信息保存:

static int parse_interface(libusb_context *ctx,
    struct libusb_interface *usb_interface, const uint8_t *buffer, int size)
{
    ...
    begin = buffer;

    /* Skip over any interface, class or vendor descriptors */
    while (size >= DESC_HEADER_LENGTH) {
        ...
        /* If we find another "proper" descriptor then we're done */
        if (header->bDescriptorType == LIBUSB_DT_INTERFACE ||
            header->bDescriptorType == LIBUSB_DT_ENDPOINT ||
            header->bDescriptorType == LIBUSB_DT_CONFIG ||
            header->bDescriptorType == LIBUSB_DT_DEVICE)
            break;

        buffer += header->bLength;
        parsed += header->bLength;
        size -= header->bLength;
    }

    /* Copy any unknown descriptors into a storage area for */
    /*  drivers to later parse */
    ptrdiff_t len = buffer - begin;
    if (len > 0) {
        void *extra = malloc((size_t)len);
        ...
        memcpy(extra, begin, (size_t)len);
        ifp->extra = extra;
        ifp->extra_length = (int)len;
    }
    ...
}

所以在uvc_scan_control里面就从接口描述符的extra信息里面去解析UVC的相关描述符:

uvc_error_t uvc_scan_control(uvc_device_t *dev, uvc_device_info_t *info) {
    ...
    for (interface_idx = 0; interface_idx < info->config->bNumInterfaces; ++interface_idx) {
        if_desc = &info->config->interface[interface_idx].altsetting[0];
        MARK("interface_idx=%d:bInterfaceClass=%02x,bInterfaceSubClass=%02x", interface_idx, if_desc->bInterfaceClass, if_desc->bInterfaceSubClass);
        // select first found Video control
        if (if_desc->bInterfaceClass == LIBUSB_CLASS_VIDEO/*14*/ && if_desc->bInterfaceSubClass == 1) // Video, Control
            break;
        ...
    }
    ...
    buffer = if_desc->extra;
    buffer_left = if_desc->extra_length;

    while (buffer_left >= 3) { // parseX needs to see buf[0,2] = length,type
        block_size = buffer[0];
        parse_ret = uvc_parse_vc(dev, info, buffer, block_size);

        if (parse_ret != UVC_SUCCESS) {
            ret = parse_ret;
            break;
        }

        buffer_left -= block_size;
        buffer += block_size;
    }

    ...
}

uvc_error_t uvc_parse_vc(uvc_device_t *dev, uvc_device_info_t *info,
        const unsigned char *block, size_t block_size) {
    int descriptor_subtype;
    uvc_error_t ret = UVC_SUCCESS;

    UVC_ENTER();

    if (block[1] != LIBUSB_DT_CS_INTERFACE/*36*/) { // not a CS_INTERFACE descriptor??
        UVC_EXIT(UVC_SUCCESS);
        return UVC_SUCCESS; // UVC_ERROR_INVALID_DEVICE;
    }

    descriptor_subtype = block[2];

    switch (descriptor_subtype) {
    case UVC_VC_HEADER:
        ret = uvc_parse_vc_header(dev, info, block, block_size);
        break;
    case UVC_VC_INPUT_TERMINAL:
        ret = uvc_parse_vc_input_terminal(dev, info, block, block_size);
        break;
    case UVC_VC_OUTPUT_TERMINAL:
        break;
    case UVC_VC_SELECTOR_UNIT:
        break;
    case UVC_VC_PROCESSING_UNIT:
        ret = uvc_parse_vc_processing_unit(dev, info, block, block_size);
        break;
    case UVC_VC_EXTENSION_UNIT:
        ret = uvc_parse_vc_extension_unit(dev, info, block, block_size);
        break;
    default:
        LOGW("UVC_ERROR_INVALID_DEVICE:descriptor_subtype=0x%02x", descriptor_subtype);
        ret = UVC_ERROR_INVALID_DEVICE;
    }

    UVC_EXIT(ret);
    return ret;
}

只要找到bInterfaceClass等于14,bInterfaceSubClass等于1的视频控制接口,然后在它的拓展信息里面找到UVC_VC_INPUT_TERMINAL(0x02)类型的描述符就是我们需要的UVC 相机终端描述符

USB通讯

前面有说到端点是实现USB设备功能的物理缓冲区实体,USB主机和设备是通过端点进行数据交互的,之前做HID设备通讯的时候流程是找到bInterfaceClassUsbConstants.USB_CLASS_HID(0x03)类型的接口,在它下面找到输入端点去写入请求,然后找到输出端点去读取设备响应。

但是UVC的摄像头控制并不是用视频控制接口去读写,而是直接使用USB设备不属于任何接口的0号端口去进行通讯。

例如uvc_get_pantilt_abs里面在传输数据的时候就没有指定端口号:

uvc_error_t uvc_get_pantilt_abs(uvc_device_handle_t *devh, int32_t *pan, int32_t *tilt,
    enum uvc_req_code req_code) {

    uint8_t data[8];
    uvc_error_t ret;

    ret = libusb_control_transfer(devh->usb_devh, REQ_TYPE_GET, req_code,
            UVC_CT_PANTILT_ABSOLUTE_CONTROL << 8,
            devh->info->ctrl_if.input_term_descs->request,
            data, sizeof(data), CTRL_TIMEOUT_MILLIS);

    if (LIKELY(ret == sizeof(data))) {
        *pan = DW_TO_INT(data);
        *tilt = DW_TO_INT(data + 4);
        return UVC_SUCCESS;
    } else {
        return ret;
    }
}

因为在libusb_control_transfer里面调用libusb_fill_control_transfer去填充信息的时候就会把端口指定为0号端口

int API_EXPORTED libusb_control_transfer(libusb_device_handle *dev_handle,
    uint8_t bmRequestType, uint8_t bRequest, uint16_t wValue, uint16_t wIndex,
    unsigned char *data, uint16_t wLength, unsigned int timeout)
{
    ...
    libusb_fill_control_transfer(transfer, dev_handle, buffer,
        sync_transfer_cb, &completed, timeout); // 填充transfer信息
    transfer->flags = LIBUSB_TRANSFER_FREE_BUFFER;
    r = libusb_submit_transfer(transfer); // 发送请求
    if (UNLIKELY(r < 0)) {
        libusb_free_transfer(transfer);
        return r;
    }

    sync_transfer_wait_for_completion(transfer); // 等待回复
    ...

}

static inline void libusb_fill_control_transfer(
    struct libusb_transfer *transfer, libusb_device_handle *dev_handle,
    unsigned char *buffer, libusb_transfer_cb_fn callback, void *user_data,
    unsigned int timeout)
{
    struct libusb_control_setup *setup = (struct libusb_control_setup *)(void *) buffer;
    transfer->dev_handle = dev_handle;
    transfer->endpoint = 0; // 指定0号端口
    ...
}

涉及到使用USB进行通讯的4种方式:

  • 控制传输 - 设备接入主机时,需要通过控制传输去获取USB设备的描述符以及对设备进行识别,在设备的枚举过程中都是使用控制传输进行数据交换。
  • 同步传输 - 也叫等时传输,用于要求数据连续、实时且数据量大的场合,其对传输延时十分敏感,类似用于USB摄像设备,USB语音设备等等。
  • 中断传输 - 用于数据量小的数据不连续的但实时性高的场合的一种传输方式,主要应用于人机交互设备(HID)中的USB鼠标和USB键盘等。
  • 批量传输 - 用于数据量大但对时间要求又不高的场合的一种传输方式,类似用于USB打印机和USB扫描仪等等。

控制传输

控制传输是usb设备一定会支持的传输方式,因为描述符就是通过这种方式获取的.

在安卓应用层我们可以通过调用UsbDeviceConnection.controlTransfer来实现,参考UVCCamera里面uvc_get_pantilt_abs里面获取PanTilt值的c代码,在java层可以用下面代码获取

private const val CONTROL_REQ_TYPE_GET = 0xa1
private const val UVC_GET_CUR = 0x81

val connection = usbManager.openDevice(device)

// 先claim bInterfaceClass为CC_VIDEO(0x0E) bInterfaceSubClass为SC_VIDEOCONTROL(0x01)的摄像头控制接口
val vcInterface = UsbUtils.getInterface(device, UsbConstants.USB_CLASS_VIDEO, USB_SUBCLASS_VIDEO_CONTROL)
connection.claimInterface(vcInterface, true)

// 然后发送控制指令获取PanTilt绝对值
val buff = ByteArray(8)
val index = getPanTiltControlIndex(connection)
val value = CT_PANTILT_ABSOLUTE_CONTROL.shl(8)
connection.controlTransfer(CONTROL_REQ_TYPE_GET, UVC_GET_CUR, value, index, buff, buff.size, 100)

// buff前四个byte组合起来是pan值
// buff后四个byte组合起来是tilt值
val pan = bytes[0].toUByte().toInt().shl(0) or
bytes[1].toUByte().toInt().shl(8) or
bytes[2].toUByte().toInt().shl(16) or
bytes[3].toUByte().toInt().shl(24)

val tilt = bytes[4].toUByte().toInt().shl(0) or
bytes[5].toUByte().toInt().shl(8) or
bytes[6].toUByte().toInt().shl(16) or
bytes[7].toUByte().toInt().shl(24)

connection.releaseInterface(usbInterface)
connection.close()

这里解释下上面的值如何来的,首先看GET_CUR的文档:

requestType request value index buffer length
10100001(接口或实体)
— — — — —
10100010(端点)
GET_CUR
GET_MIN
GET_MAX
GET_RES
GET_LEN
GET_INFO
GET_DEF
UVC中大多数情况下取值都为控制选择器CS(高字节),低字节为零。当实体ID取不同值时则该字段取值也会有所不同 实体ID(高字节)、接口(低字节)
— — — — —
端点(低字节)
用来接收数据或者发送数据的buffer buffer的大小

value

例如我们现在要获取PanTilt的绝对值,那么在value字段部分文档里面可以看到当Entity ID值为Camera Terminal时:

ControlSelector Value
... ...
CT_PANTILT_ABSOLUTE_CONTROL 0x0D
... ...

又因为value的值为控制选择器CS(高字节),低字节为零。所以value的值应该是0x0D << 8

index

Entity ID值为Camera Terminal指的是终端描述符的bTerminalID, 由于它属于控制接口描述符的extra信息,所以还需要指的该接口的bInterfaceNunber:

val USB_DESC_TYPE_INTERFACE_LEN = 9.toByte()
val USB_DESC_TYPE_INTERFACE = 0x04.toByte()
val USB_DESC_TYPE_CS_INTERFACE = 0x24.toByte()
val USB_DESC_SUB_TYPE_VC_INPUT_TERMINAL = 0x02.toByte()
val USB_SUBCLASS_VIDEO_CONTROL = 0x01

private fun getPanTiltControlIndex(connection: UsbDeviceConnection): Int {
    val desc = connection.rawDescriptors ?: return -1

    var index = 0
    var isInVideoControlInterface = false
    var interfaceNumber = 0
    while (index < desc.size) {
        if (desc[index] == USB_DESC_TYPE_INTERFACE_LEN
            && desc[index + 1] == USB_DESC_TYPE_INTERFACE
            && desc[index + 5] == UsbConstants.USB_CLASS_VIDEO.toByte()
            && desc[index + 6] == USB_SUBCLASS_VIDEO_CONTROL.toByte()
        ) {
            // 找到bInterfaceClass为CC_VIDEO(0x0E) bInterfaceSubClass为SC_VIDEOCONTROL(0x01)的摄像头控制接口的interfaceNumber
            isInVideoControlInterface = true
            interfaceNumber = desc[index + 2].toInt()
        } else if (isInVideoControlInterface) {
            if (desc[index + 1] != USB_DESC_TYPE_CS_INTERFACE) {
                return -1
            }
            if (desc[index + 2] == USB_DESC_SUB_TYPE_VC_INPUT_TERMINAL) {
                // 在摄像头控制接口下找到bDescriptorType为CS_INTERFACE(0x24) bDescriptorSubtype为VC_INPUT_TERMINAL(0x02)的摄像头终端描述符
                // 获取它的bTerminalID用来和前面获取到的摄像头控制接口的interfaceNumber拼接成index
                return desc[index + 3].toInt().shl(8).or(interfaceNumber)
            }
        }

        index += desc[index]
    }
    return -1
}

request

我们要获取的是当前值所以request是GET_CUR(0x81),其他值的定如下:

名称 说明
RC_UNDEFINED 0x00 未定义
SET_CUR 0x01 设置属性
GET_CUR 0x81 获取当前属性
GET_MIN 0x82 获取最小设置属性
GET_MAX 0x83 获取最大设置属性
GET_RES 0x84 获取分辨率属性
GET_LEN 0x85 获取数据长度属性
GET_INF 0x86 获取设备支持的特定类请求属性
GET_DEF 0x87 获取默认属性

requestType

最后再来看requestType,由于index需要选择的是实体ID(高字节)、接口(低字节)所以requestType应该是10100001(接口或实体)。它的值这么奇怪是因为requestType的每个bit都是有意义的:

1.png

由于命令接受者为接口,所以我们在发送控制指令前还是需要找到这个接口用claimInterface去锁定它。

使用uvc控制指令的坑

似乎是因为使用安卓的Camera2等接口去读取摄像头画面的时候会使用到这个控制接口,所以如果在预览的时候去claimInterface锁定它就会造成画面卡死。

看起来似乎需要完全使用uvc自己从视频流接口读取画面,而不能一半用uvc去控制摄像头另一半用安卓原生api去获取预览画面。或者用取巧的方法在发送控制指令的时候先停止预览,发送完再开始。

其他三种传输

其他三种传输都是需要找到对应的端点才能进行通讯的,所以需要先获取到端点信息.用UsbInterface.getEndpoint去遍历接口下的端点,然后判断端点的类型和读写方向:

for (i in 0 until usbInterface.endpointCount) {
    val usbEndpoint = usbInterface.getEndpoint(i)
    when (usbEndpoint.type) {
        UsbConstants.USB_ENDPOINT_XFER_BULK -> {
            // 批量传输
            if (usbEndpoint.direction == UsbConstants.USB_DIR_OUT) {
                // 可写入端点
            } else if (usbEndpoint.direction == UsbConstants.USB_DIR_IN) {
                // 可读取端点
            }
        }
        UsbConstants.USB_ENDPOINT_XFER_ISOC -> {
            // 中断传输
        }
        UsbConstants.USB_ENDPOINT_XFER_INT -> {
            // 同步传输
        }
    }
}

他们最终都是通过UsbDeviceConnection.bulkTransfer去调用的,例如可以先写入请求在读取响应:

val requestBuffer = ByteArray(256)
// 将数据保存到requestBuffer
// 然后往写入端点写入请求数据
connection.bulkTransfer(outPoint, sendBuff, sendBuff.size, timeout)

// 从读取端点读取响应
val responseBuffer = ByteArray(256)
connection.bulkTransfer(inPoint, responseBuffer, responseBuffer.size, timeout)
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,088评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,715评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,361评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,099评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 60,987评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,063评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,486评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,175评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,440评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,518评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,305评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,190评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,550评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,880评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,152评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,451评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,637评论 2 335

推荐阅读更多精彩内容