安卓UVC控制協(xié)議入門

最近的項目里面需要對UVC攝像頭進行操控,簡單的了解了下相關的知識。

首先UVC全稱為USB video(device) class,是微軟與另外幾家設備廠商聯(lián)合推出的為USB視頻捕獲設備定義的協(xié)議標準,目前已成為USB org標準之一。在USB中文網(wǎng)上其實有比較詳細的描述,但是新手直接上來就看這個協(xié)議其實是比較懵逼的筹淫。所以可以參考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數(shù)組,我們先要理解什么是usb描述符才能去解析它。usb描述符其實就是描述usb的屬性和用途的,四種主要描述符的邏輯結構大概如下:

1.png
  • 設備描述符: 每一個USB設備只有一個設備描述符浇冰,主要向主機說明設備類型贬媒、端點0最大包長、設備版本肘习、配置數(shù)量等等
  • 配置描述符: 每一個USB設備至少有一個或者多個配置描述符,但是主機同一時間只能選擇某一種配置,標準配置描述符主要向主機描述當前配置下的設備屬性若专、所需電流豁翎、支持的接口數(shù)、配置描述符集合長度等等投蝉。
  • 接口描述符 : 每一個USB配置下至少有一個或者多個接口描述符养葵,接口描述符主要說明設備類型、此接口下使用的端點數(shù)(不包括0號端點)墓拜,一個接口就是實現(xiàn)一種功能港柜,實現(xiàn)這種功能可能需要端點0就夠了,可能還需要其它的端點配合咳榜。
  • 端點描述符: 每一個USB接口下可以有多個端點描述符夏醉,端點描述符用來描述符端點的各種屬性。

端點是實現(xiàn)USB設備功能的物理緩沖區(qū)實體涌韩,USB主機和設備是通過端點進行數(shù)據(jù)交互的

描述符解析

所有類型的描述符的前兩個字節(jié)定義都是一樣的,第一個字節(jié)指定描述符的長度畔柔,而第二個字節(jié)表示描述符類型。所以我們拿到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標準里找到它具體的數(shù)據(jù)結構去解析,例如設備描述符的定義如下:

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:自動曝光優(yōu)先級
  • 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通訊

前面有說到端點是實現(xiàn)USB設備功能的物理緩沖區(qū)實體雇毫,USB主機和設備是通過端點進行數(shù)據(jù)交互的,之前做HID設備通訊的時候流程是找到bInterfaceClassUsbConstants.USB_CLASS_HID(0x03)類型的接口,在它下面找到輸入端點去寫入請求,然后找到輸出端點去讀取設備響應玄捕。

但是UVC的攝像頭控制并不是用視頻控制接口去讀寫,而是直接使用USB設備不屬于任何接口的0號端口去進行通訊。

例如uvc_get_pantilt_abs里面在傳輸數(shù)據(jù)的時候就沒有指定端口號:

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); // 發(fā)送請求
    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設備的描述符以及對設備進行識別枚粘,在設備的枚舉過程中都是使用控制傳輸進行數(shù)據(jù)交換。
  • 同步傳輸 - 也叫等時傳輸,用于要求數(shù)據(jù)連續(xù)飘蚯、實時且數(shù)據(jù)量大的場合馍迄,其對傳輸延時十分敏感福也,類似用于USB攝像設備,USB語音設備等等攀圈。
  • 中斷傳輸 - 用于數(shù)據(jù)量小的數(shù)據(jù)不連續(xù)的但實時性高的場合的一種傳輸方式暴凑,主要應用于人機交互設備(HID)中的USB鼠標和USB鍵盤等。
  • 批量傳輸 - 用于數(shù)據(jù)量大但對時間要求又不高的場合的一種傳輸方式赘来,類似用于USB打印機和USB掃描儀等等现喳。

控制傳輸

控制傳輸是usb設備一定會支持的傳輸方式,因為描述符就是通過這種方式獲取的.

在安卓應用層我們可以通過調用UsbDeviceConnection.controlTransfer來實現(xiàn),參考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)

// 然后發(fā)送控制指令獲取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中大多數(shù)情況下取值都為控制選擇器CS(高字節(jié)),低字節(jié)為零撕捍。當實體ID取不同值時則該字段取值也會有所不同 實體ID(高字節(jié))拿穴、接口(低字節(jié))
— — — — —
端點(低字節(jié))
用來接收數(shù)據(jù)或者發(fā)送數(shù)據(jù)的buffer buffer的大小

value

例如我們現(xiàn)在要獲取PanTilt的絕對值,那么在value字段部分文檔里面可以看到當Entity ID值為Camera Terminal時:

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

又因為value的值為控制選擇器CS(高字節(jié)),低字節(jié)為零忧风。所以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 獲取數(shù)據(jù)長度屬性
GET_INF 0x86 獲取設備支持的特定類請求屬性
GET_DEF 0x87 獲取默認屬性

requestType

最后再來看requestType,由于index需要選擇的是實體ID(高字節(jié))默色、接口(低字節(jié))所以requestType應該是10100001(接口或實體)。它的值這么奇怪是因為requestType的每個bit都是有意義的:

1.png

由于命令接受者為接口,所以我們在發(fā)送控制指令前還是需要找到這個接口用claimInterface去鎖定它狮腿。

使用uvc控制指令的坑

似乎是因為使用安卓的Camera2等接口去讀取攝像頭畫面的時候會使用到這個控制接口,所以如果在預覽的時候去claimInterface鎖定它就會造成畫面卡死腿宰。

看起來似乎需要完全使用uvc自己從視頻流接口讀取畫面,而不能一半用uvc去控制攝像頭另一半用安卓原生api去獲取預覽畫面≡迪幔或者用取巧的方法在發(fā)送控制指令的時候先停止預覽,發(fā)送完再開始吃度。

其他三種傳輸

其他三種傳輸都是需要找到對應的端點才能進行通訊的,所以需要先獲取到端點信息.用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)
// 將數(shù)據(jù)保存到requestBuffer
// 然后往寫入端點寫入請求數(shù)據(jù)
connection.bulkTransfer(outPoint, sendBuff, sendBuff.size, timeout)

// 從讀取端點讀取響應
val responseBuffer = ByteArray(256)
connection.bulkTransfer(inPoint, responseBuffer, responseBuffer.size, timeout)
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市贴硫,隨后出現(xiàn)的幾起案子椿每,更是在濱河造成了極大的恐慌,老刑警劉巖英遭,帶你破解...
    沈念sama閱讀 206,839評論 6 482
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件间护,死亡現(xiàn)場離奇詭異,居然都是意外死亡挖诸,警方通過查閱死者的電腦和手機汁尺,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,543評論 2 382
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來多律,“玉大人痴突,你說我怎么就攤上這事±擒瘢” “怎么了辽装?”我有些...
    開封第一講書人閱讀 153,116評論 0 344
  • 文/不壞的土叔 我叫張陵,是天一觀的道長相味。 經常有香客問我拾积,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,371評論 1 279
  • 正文 為了忘掉前任殷勘,我火速辦了婚禮,結果婚禮上昔搂,老公的妹妹穿的比我還像新娘玲销。我一直安慰自己,他們只是感情好摘符,可當我...
    茶點故事閱讀 64,384評論 5 374
  • 文/花漫 我一把揭開白布逛裤。 她就那樣靜靜地躺著,像睡著了一般带族。 火紅的嫁衣襯著肌膚如雪锁荔。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,111評論 1 285
  • 那天阳堕,我揣著相機與錄音择克,去河邊找鬼恬总。 笑死,一個胖子當著我的面吹牛肚邢,可吹牛的內容都是我干的。 我是一名探鬼主播骡湖,決...
    沈念sama閱讀 38,416評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼勺鸦,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了懊渡?” 一聲冷哼從身側響起军拟,我...
    開封第一講書人閱讀 37,053評論 0 259
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎肾档,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體俗慈,經...
    沈念sama閱讀 43,558評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡闺阱,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 36,007評論 2 325
  • 正文 我和宋清朗相戀三年舵变,在試婚紗的時候發(fā)現(xiàn)自己被綠了纪隙。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,117評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡碘饼,死狀恐怖悲伶,靈堂內的尸體忽然破棺而出拢切,到底是詐尸還是另有隱情,我是刑警寧澤淮椰,帶...
    沈念sama閱讀 33,756評論 4 324
  • 正文 年R本政府宣布主穗,位于F島的核電站,受9級特大地震影響争拐,放射性物質發(fā)生泄漏晦雨。R本人自食惡果不足惜闹瞧,卻給世界環(huán)境...
    茶點故事閱讀 39,324評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望万牺。 院中可真熱鬧,春花似錦覆旱、人聲如沸核无。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,315評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至佑惠,卻和暖如春齐疙,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背赌厅。 一陣腳步聲響...
    開封第一講書人閱讀 31,539評論 1 262
  • 我被黑心中介騙來泰國打工特愿, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留勾缭,地道東北人。 一個月前我還...
    沈念sama閱讀 45,578評論 2 355
  • 正文 我出身青樓毒嫡,卻偏偏與公主長得像兜畸,于是被迫代替她去往敵國和親碘梢。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 42,877評論 2 345

推薦閱讀更多精彩內容