OMX Service Codec加载

OMX Service Codec加载

看一个总体的框架图

image-20210906100244903.png

图来自 https://blog.csdn.net/VNanyesheshou/article/details/115027180

我这看的平台是Android 9

av/services/mediacodec/main_codecservice.cpp

    // Registration of customized codec services
    void *registrantLib = dlopen(
            "libmedia_codecserviceregistrant.so",
            RTLD_NOW | RTLD_LOCAL);
    if (registrantLib) {
        RegisterCodecServicesFunc registerCodecServices =
                reinterpret_cast<RegisterCodecServicesFunc>(
                dlsym(registrantLib, "RegisterCodecServices"));
        if (registerCodecServices) {
            registerCodecServices();
        } else {
            LOG(WARNING) << "Cannot register additional services "
                    "-- corrupted library.";
        }
    } else {
        // Default codec services
        using namespace ::android::hardware::media::omx::V1_0;
        sp<IOmxStore> omxStore = new implementation::OmxStore();
        if (omxStore == nullptr) {
            LOG(ERROR) << "Cannot create IOmxStore HAL service.";
        } else if (omxStore->registerAsService() != OK) {
            LOG(ERROR) << "Cannot register IOmxStore HAL service.";
        }
        sp<IOmx> omx = new implementation::Omx();
        if (omx == nullptr) {
            LOG(ERROR) << "Cannot create IOmx HAL service.";
        } else if (omx->registerAsService() != OK) {
            LOG(ERROR) << "Cannot register IOmx HAL service.";
        } else {
            LOG(INFO) << "IOmx HAL service created.";
        }
    }

我们这里主要看默认的case, 创建OmxStore, Omx, 分别注册服务

OmxStore 初始化解析

sp<IOmxStore> omxStore = new implementation::OmxStore();

上述omxStore调用了默认的构造函数,看到 cpp中的实现都是带参数的

frameworks/av/media/libstagefright/omx/1.0/OmxStore.cpp

OmxStore::OmxStore(
        const char* owner,
        const char* const* searchDirs,
        const char* mainXmlName,
        const char* performanceXmlName,
        const char* profilingResultsXmlPath) {
    MediaCodecsXmlParser parser(searchDirs,
            mainXmlName,
            performanceXmlName,
            profilingResultsXmlPath);
    mParsingStatus = toStatus(parser.getParsingStatus());

    const auto& serviceAttributeMap = parser.getServiceAttributeMap();
    mServiceAttributeList.resize(serviceAttributeMap.size());
    size_t i = 0;
    for (const auto& attributePair : serviceAttributeMap) {
        ServiceAttribute attribute;
        attribute.key = attributePair.first;
        attribute.value = attributePair.second;
        mServiceAttributeList[i] = std::move(attribute);
        ++i;
    }

    const auto& roleMap = parser.getRoleMap();
    mRoleList.resize(roleMap.size());
    i = 0;
    for (const auto& rolePair : roleMap) {
        RoleInfo role;
        role.role = rolePair.first;
        role.type = rolePair.second.type;
        role.isEncoder = rolePair.second.isEncoder;
        // TODO: Currently, preferPlatformNodes information is not available in
        // the xml file. Once we have a way to provide this information, it
        // should be parsed properly.
        role.preferPlatformNodes = rolePair.first.compare(0, 5, "audio") == 0;
        hidl_vec<NodeInfo>& nodeList = role.nodes;
        nodeList.resize(rolePair.second.nodeList.size());
        size_t j = 0;
        for (const auto& nodePair : rolePair.second.nodeList) {
            NodeInfo node;
            node.name = nodePair.second.name;
            node.owner = owner;
            hidl_vec<NodeAttribute>& attributeList = node.attributes;
            attributeList.resize(nodePair.second.attributeList.size());
            size_t k = 0;
            for (const auto& attributePair : nodePair.second.attributeList) {
                NodeAttribute attribute;
                attribute.key = attributePair.first;
                attribute.value = attributePair.second;
                attributeList[k] = std::move(attribute);
                ++k;
            }
            nodeList[j] = std::move(node);
            ++j;
        }
        mRoleList[i] = std::move(role);
        ++i;
    }

    mPrefix = parser.getCommonPrefix();
}

于是去.h 中看看默认参数是什么,这里指定了默认参数在 MediaCodecsXmlParser 中定义

struct OmxStore : public IOmxStore {
    OmxStore(
            const char* owner = "default",
            const char* const* searchDirs
                = MediaCodecsXmlParser::defaultSearchDirs,
            const char* mainXmlName
                = MediaCodecsXmlParser::defaultMainXmlName,
            const char* performanceXmlName
                = MediaCodecsXmlParser::defaultPerformanceXmlName,
            const char* profilingResultsXmlPath
                = MediaCodecsXmlParser::defaultProfilingResultsXmlPath);
    // ...            
    }

在 MediaCodecsXmlParser.h 路劲中可以看到默认路径定义如下

    // Treblized media codec list will be located in /odm/etc or /vendor/etc.
    static constexpr char const* defaultSearchDirs[] =
            {"/odm/etc", "/vendor/etc", "/etc", nullptr};
    static constexpr char const* defaultMainXmlName =
            "media_codecs.xml";
    static constexpr char const* defaultPerformanceXmlName =
            "media_codecs_performance.xml";
    static constexpr char const* defaultProfilingResultsXmlPath =
            "/data/misc/media/media_codecs_profiling_results.xml";

具体再去跟踪 cpp中的构造函数,可以看到 OMXStore初始化通过MediaCodecsXmlParser加载media_codecs.xml、media_codecs_performance.xml,并将信息保存在mServiceAttributeList、mRoleList中

OMX 初始化解析

初始化完OmxStore后,接下来就初始化 OMX的主要服务了

Omx::Omx() :
    mMaster(new OMXMaster()),
    mParser() {
}

OMXMaster 加载和管理 plugin,包括硬编码和软编码,这里主要是 addVendorPlugin 和 addPlugin(new SoftOMXPlugin); 这两个函数的实现

OMXMaster::OMXMaster()
    : mVendorLibHandle(NULL) {

    pid_t pid = getpid();
    char filename[20];
    snprintf(filename, sizeof(filename), "/proc/%d/comm", pid);
    int fd = open(filename, O_RDONLY);
    if (fd < 0) {
      ALOGW("couldn't determine process name");
      strlcpy(mProcessName, "<unknown>", sizeof(mProcessName));
    } else {
      ssize_t len = read(fd, mProcessName, sizeof(mProcessName));
      if (len < 2) {
        ALOGW("couldn't determine process name");
        strlcpy(mProcessName, "<unknown>", sizeof(mProcessName));
      } else {
        // the name is newline terminated, so erase the newline
        mProcessName[len - 1] = 0;
      }
      close(fd);
    }

    addVendorPlugin();
    addPlugin(new SoftOMXPlugin);
}

Vendor Plugin

addVendorPlugin加载共享库libstagefrighthw.so,查找createOMXPlugin函数,并调用createOMXPlugin 获得OMXPluginBase类型对象,然后调用 addPlugin((*createOMXPlugin)());

以Mstar/Mtk 为例,libstagefrighthw 的实现是在 MS_OMX_Plugin.cpp这个文件中

Android.mk

LOCAL_MODULE := libstagefrighthw

LOCAL_SRC_FILES := MS_OMX_Plugin.cpp

MS_OMX_Plugin.cpp

OMXPluginBase *createOMXPlugin() {
    return new MSOMXPlugin;
}

OMXPluginBase 是有Android 定义的,用来对接hal vendor接口的实现, 其主要包含四个虚拟方法, 硬件厂商接入自己的编解码器,需要继承OMXPluginBase 类,并实现抽象方法。

frameworks/native/headers/media_plugin/media/hardware/OMXPluginBase.h

struct OMXPluginBase {
    OMXPluginBase() {}
    virtual ~OMXPluginBase() {}

    virtual OMX_ERRORTYPE makeComponentInstance(
            const char *name,
            const OMX_CALLBACKTYPE *callbacks,
            OMX_PTR appData,
            OMX_COMPONENTTYPE **component) = 0;

    virtual OMX_ERRORTYPE destroyComponentInstance(
            OMX_COMPONENTTYPE *component) = 0;

    virtual OMX_ERRORTYPE enumerateComponents(
            OMX_STRING name,
            size_t size,
            OMX_U32 index) = 0;

    virtual OMX_ERRORTYPE getRolesOfComponent(
            const char *name,
            Vector<String8> *roles) = 0;

private:
    OMXPluginBase(const OMXPluginBase &);
    OMXPluginBase &operator=(const OMXPluginBase &);
};

更多vendor的细节可以去 MS_OMX_Plugin.cpp 里面寻找

Software Plugin

Software Plugin 定义是在 SoftOMXPlugin.cpp,其接口和定义同样遵循 OMXPluginBase 结构的要求,同样需要实现上述的OMXPluginBase.h 抽象方法

struct SoftOMXPlugin : public OMXPluginBase {
}

而 Software Plugin 定义的组件支持在这里

static const struct {
    const char *mName;
    const char *mLibNameSuffix;
    const char *mRole;

} kComponents[] = {
    // two choices for aac decoding.
    // configurable in media/libstagefright/data/media_codecs_google_audio.xml
    // default implementation
    { "OMX.google.aac.decoder", "aacdec", "audio_decoder.aac" },
    // alternate implementation
    { "OMX.google.xaac.decoder", "xaacdec", "audio_decoder.aac" },
    { "OMX.google.aac.encoder", "aacenc", "audio_encoder.aac" },
    { "OMX.google.amrnb.decoder", "amrdec", "audio_decoder.amrnb" },
    { "OMX.google.amrnb.encoder", "amrnbenc", "audio_encoder.amrnb" },
    { "OMX.google.amrwb.decoder", "amrdec", "audio_decoder.amrwb" },
    { "OMX.google.amrwb.encoder", "amrwbenc", "audio_encoder.amrwb" },
    { "OMX.google.h264.decoder", "avcdec", "video_decoder.avc" },
    { "OMX.google.h264.encoder", "avcenc", "video_encoder.avc" },
    { "OMX.google.hevc.decoder", "hevcdec", "video_decoder.hevc" },
    { "OMX.google.g711.alaw.decoder", "g711dec", "audio_decoder.g711alaw" },
    { "OMX.google.g711.mlaw.decoder", "g711dec", "audio_decoder.g711mlaw" },
    { "OMX.google.mpeg2.decoder", "mpeg2dec", "video_decoder.mpeg2" },
    { "OMX.google.h263.decoder", "mpeg4dec", "video_decoder.h263" },
    { "OMX.google.h263.encoder", "mpeg4enc", "video_encoder.h263" },
    { "OMX.google.mpeg4.decoder", "mpeg4dec", "video_decoder.mpeg4" },
    { "OMX.google.mpeg4.encoder", "mpeg4enc", "video_encoder.mpeg4" },
    { "OMX.google.mp3.decoder", "mp3dec", "audio_decoder.mp3" },
    { "OMX.google.vorbis.decoder", "vorbisdec", "audio_decoder.vorbis" },
    { "OMX.google.opus.decoder", "opusdec", "audio_decoder.opus" },
    { "OMX.google.vp8.decoder", "vpxdec", "video_decoder.vp8" },
    { "OMX.google.vp9.decoder", "vpxdec", "video_decoder.vp9" },
    { "OMX.google.vp8.encoder", "vpxenc", "video_encoder.vp8" },
    { "OMX.google.vp9.encoder", "vpxenc", "video_encoder.vp9" },
    { "OMX.google.raw.decoder", "rawdec", "audio_decoder.raw" },
    { "OMX.google.flac.decoder", "flacdec", "audio_decoder.flac" },
    { "OMX.google.flac.encoder", "flacenc", "audio_encoder.flac" },
    { "OMX.google.gsm.decoder", "gsmdec", "audio_decoder.gsm" },
};

这里的例子是加载了两个插件,当然也可以根据需要再添加更多的插件进来,而每个插件中还有组件的定义,比如说 SoftOMXPlugin 这个插件里面 有支持OMX.google.h264.decoder 的组件和 OMX.google.hevc.decoder,在这里面也可以添加想要支持的组件

回到 *addPlugin(OMXPluginBase plugin) 方法,可以先看下 OMXMaster.h中定义的数据结构

    List<OMXPluginBase *> mPlugins;
    KeyedVector<String8, OMXPluginBase *> mPluginByComponentName;
    KeyedVector<OMX_COMPONENTTYPE *, OMXPluginBase *> mPluginByInstance;

结合如下函数方法可以看到 mPlugins 维护了目前系统之有多少组插件(Vendor, Software ....), 而 mPluginByComponentName 维护了每个插件中支持的组件能力

void OMXMaster::addPlugin(OMXPluginBase *plugin) {
    Mutex::Autolock autoLock(mLock);

    mPlugins.push_back(plugin);

    OMX_U32 index = 0;

    char name[128];
    OMX_ERRORTYPE err;
    while ((err = plugin->enumerateComponents(
                    name, sizeof(name), index++)) == OMX_ErrorNone) {
        String8 name8(name);

        if (mPluginByComponentName.indexOfKey(name8) >= 0) {
            ALOGE("A component of name '%s' already exists, ignoring this one.",
                 name8.string());

            continue;
        }

        mPluginByComponentName.add(name8, plugin);
    }

    if (err != OMX_ErrorNoMore) {
        ALOGE("OMX plugin failed w/ error 0x%08x after registering %zu "
             "components", err, mPluginByComponentName.size());
    }
}

而 mPluginByInstance 这个数据结构则要看如下 makeComponentInstance 这个函数的实现,这个函数是实例化组件中的插件,比如说要初始化 OMX.google.h264.decoder这个组件,就会调用 makeComponentInstance(),而如果实例化成功了,即 err = OMX_ErrorNone 就会添加进来到 mPluginByInstance。

OMX_ERRORTYPE OMXMaster::makeComponentInstance(
        const char *name,
        const OMX_CALLBACKTYPE *callbacks,
        OMX_PTR appData,
        OMX_COMPONENTTYPE **component) {
    ALOGI("makeComponentInstance(%s) in %s process", name, mProcessName);
    Mutex::Autolock autoLock(mLock);

    *component = NULL;

    ssize_t index = mPluginByComponentName.indexOfKey(String8(name));

    if (index < 0) {
        return OMX_ErrorInvalidComponentName;
    }

    OMXPluginBase *plugin = mPluginByComponentName.valueAt(index);
    OMX_ERRORTYPE err =
        plugin->makeComponentInstance(name, callbacks, appData, component);

    if (err != OMX_ErrorNone) {
        return err;
    }

    mPluginByInstance.add(*component, plugin);

    return err;
}

那么什么时候会出现 makeComponentInstance 失败了呢?一个常见的例子是hw codec 如果没有hashkey lisence 就会初始初始化,可以看到目前平台简单的流程如下

makeComponentInstance --> mGetHandle --> MS_OMX_GetHandle --> OMX_GetHandle -- > check from MS_OMX_ScanSupportedCodec

01-01 00:00:01.652  3321  3499 I OMXClient: IOmx service obtained
01-01 00:00:01.653  3336  3921 I OMXMaster: makeComponentInstance(OMX.MS.WMA.Decoder) in omx@1.0-service process
01-01 00:00:01.653  3336  3921 E media.codec: Failed to allocate omx component 'OMX.MS.WMA.Decoder'  err=InvalidComponentName(0x80001002)
01-01 00:00:01.653  3321  3499 W OmxInfoBuilder: Fail to add mime audio/x-ms-wma to codec OMX.MS.WMA.Decoder after software codecs

那么什么时候会调取用到 makeComponentInstance ?可以接着往下看

MediaCodecList初始化

前面介绍了一些 OMX的初始化,包括解析media_codec.xml 数据,那么什么时候会使用到解析的数据并如何更新到系统中

MediaCodecList::MediaCodecList(std::vector<MediaCodecListBuilderBase*> builders) {
        mInitCheck = builder->buildMediaCodecList(&writer);
    }

frameworks/av/media/libstagefright/OmxInfoBuilder.cpp 先去获取前面OmxStore 解析的media_codec.xml中支持的codec 列表,即 roles 和 serviceAttributes 数据

status_t OmxInfoBuilder::buildMediaCodecList(MediaCodecListWriter* writer) {
    // List service attributes (global settings)
    Status status;
    hidl_vec<IOmxStore::RoleInfo> roles;
    auto transStatus = omxStore->listRoles(
            [&roles] (
            const hidl_vec<IOmxStore::RoleInfo>& inRoleList) {
                roles = inRoleList;
            });

    hidl_vec<IOmxStore::ServiceAttribute> serviceAttributes;
    transStatus = omxStore->listServiceAttributes(
            [&status, &serviceAttributes] (
            Status inStatus,
            const hidl_vec<IOmxStore::ServiceAttribute>& inAttributes) {
                status = inStatus;
                serviceAttributes = inAttributes;
            });
     // 接下来遍历 xml 中的节点数据 
     for (const IOmxStore::RoleInfo& role : roles) {
        const hidl_string& typeName = role.type;
        bool isEncoder = role.isEncoder;    // xml中有有声明encoder和decoder 的支持组件
        bool preferPlatformNodes = role.preferPlatformNodes; 
        /* 这个属性注释如下,如果优先平台的组件需要添加到软解组件下面,下面的判断用回到 preferPlatformNodes */
        // If preferPlatformNodes is true, hardware nodes must be added after
        // platform (software) nodes. hwCodecs is used to hold hardware nodes
        // that need to be added after software nodes for the same role.
        std::vector<const IOmxStore::NodeInfo*> hwCodecs;
        for (const IOmxStore::NodeInfo& node : role.nodes) {
            const hidl_string& nodeName = node.name;
            bool isSoftware = hasPrefix(nodeName, "OMX.google");
            MediaCodecInfoWriter* info;
            // 对软解组件和硬解组件分别归类到 swCodecName2Info、hwCodecName2Info

        }
        // If preferPlatformNodes is true, hardware nodes will not have been
        // added in the loop above, but rather saved in hwCodecs. They are
        // going to be added here.
        if (preferPlatformNodes) {
            for (const IOmxStore::NodeInfo *node : hwCodecs) {
                MediaCodecInfoWriter* info;
                const hidl_string& nodeName = node->name;
                auto c2i = hwCodecName2Info.find(nodeName);

                std::unique_ptr<MediaCodecInfo::CapabilitiesWriter> caps =
                        info->addMime(typeName.c_str());
                if (queryCapabilities(
                        *node, typeName.c_str(), isEncoder, caps.get()) != OK) {
                    ALOGW("Fail to add mime %s to codec %s "
                          "after software codecs",
                          typeName.c_str(), nodeName.c_str());
                    info->removeMime(typeName.c_str());
                }
            }
        }
     }
}

上面的 buildMediaCodecList 这个函数挺长的,过了一遍下来后其主要是获取media_codec.xml 中解析的节点组件数据,然后还有一个重要的作用就是将硬件的组件的支持能力更新到MediaCodecInfo::mCaps 中

简单介绍下这个数据接口的作用 ,方便理解里面的逻辑

MediaCodecInfo:: KeyedVector<AString, sp<Capabilities> > mCaps; 

这是一个Vector 数组,不过带有支持的组件名,举个例子

mCaps[2] = {
    <"OMX.MS.AC3.Decoder", Capabilities(audio/ac3)>,
    <"OMX.MS.AC3.Decoder", Capabilities(audio/ac3p)>
}

通过 info->addMime 添加 audio/ac3p 到 OMX.MS.AC3.Decoder组件

然后 queryCapabilities 调用 makeComponentInstance等流程检查平台是否支持

如果不支持则 info->removeMime 移除OMX.MS.AC3.Decoder 对 audio/ac3p的支持

而这个mCaps 的MediaCodecInfo会进一步提供给 MediaCodecList::findCodecByType 和 MediaCodecList::findMatchingCodecs 使用,然后进一步封装后给player在初始化decoder的时候用于查找目前系统支持的codec 组件

看MediaCodecList 这块过程中打开了一些打印调试和理清

diff --git a/frameworks/av/media/libmedia/MediaCodecInfo.cpp b/frameworks/av/media/libmedia/MediaCodecInfo.cpp
index 26588c0..c74c39d 100644
--- a/frameworks/av/media/libmedia/MediaCodecInfo.cpp
+++ b/frameworks/av/media/libmedia/MediaCodecInfo.cpp
@@ -14,7 +14,7 @@
  * limitations under the License.
  */

-//#define LOG_NDEBUG 0
+#define LOG_NDEBUG 0
 #define LOG_TAG "MediaCodecInfo"
 #include <utils/Log.h>

@@ -192,7 +192,9 @@ void MediaCodecInfo::getSupportedMimes(Vector<AString> *mimes) const {

 const sp<MediaCodecInfo::Capabilities>
 MediaCodecInfo::getCapabilitiesFor(const char *mime) const {
+    ALOGD("%s Enter", __FUNCTION__);
     ssize_t ix = getCapabilityIndex(mime);
+    ALOGD("%s ix:%d", __FUNCTION__, ix);
     if (ix >= 0) {
         return mCaps.valueAt(ix);
     }
@@ -225,6 +227,8 @@ sp<MediaCodecInfo> MediaCodecInfo::FromParcel(const Parcel &parcel) {
         if (caps == NULL)
             return NULL;
         if (info != NULL) {
+            ALOGD("%s %d codec:%s mime:%s",
+                 __FUNCTION__, __LINE__, info->mName.c_str(), mime.c_str());
             info->mCaps.add(mime, caps);
         }
     }
@@ -246,7 +250,9 @@ status_t MediaCodecInfo::writeToParcel(Parcel *parcel) const {

 ssize_t MediaCodecInfo::getCapabilityIndex(const char *mime) const {
     if (mime) {
+        ALOGD("%s mCaps.size:%d", __FUNCTION__, mCaps.size());
         for (size_t ix = 0; ix < mCaps.size(); ix++) {
+            ALOGD("search mime:%s in [%d] %s", mime, ix, mCaps.keyAt(ix).c_str());
             if (mCaps.keyAt(ix).equalsIgnoreCase(mime)) {
                 return ix;
             }
@@ -284,6 +290,8 @@ std::unique_ptr<MediaCodecInfo::CapabilitiesWriter>
     }
     sp<MediaCodecInfo::Capabilities> caps = new MediaCodecInfo::Capabilities();
     mInfo->mCaps.add(AString(mime), caps);
+    ALOGD("%s %d codec:%s mime:%s",
+                 __FUNCTION__, __LINE__, mInfo->mName.c_str(), mime);
     return std::unique_ptr<MediaCodecInfo::CapabilitiesWriter>(
             new MediaCodecInfo::CapabilitiesWriter(caps.get()));
 }
@@ -291,6 +299,8 @@ std::unique_ptr<MediaCodecInfo::CapabilitiesWriter>
 bool MediaCodecInfoWriter::removeMime(const char *mime) {
     ssize_t ix = mInfo->getCapabilityIndex(mime);
     if (ix >= 0) {
+        ALOGD("%s %d codec:%s mime:%s",
+                 __FUNCTION__, __LINE__, mInfo->mName.c_str(), mime);
         mInfo->mCaps.removeItemsAt(ix);
         return true;
     }
diff --git a/frameworks/av/media/libstagefright/MediaCodecList.cpp b/frameworks/av/media/libstagefright/MediaCodecList.cpp
index eaff283..ffe3083 100644
--- a/frameworks/av/media/libstagefright/MediaCodecList.cpp
+++ b/frameworks/av/media/libstagefright/MediaCodecList.cpp
@@ -14,7 +14,7 @@
  * limitations under the License.
  */

-//#define LOG_NDEBUG 0
+#define LOG_NDEBUG 0
 #define LOG_TAG "MediaCodecList"
 #include <utils/Log.h>

@@ -243,8 +243,10 @@ ssize_t MediaCodecList::findCodecByType(
     size_t numCodecInfos = mCodecInfos.size();
     for (; startIndex < numCodecInfos; ++startIndex) {
         const MediaCodecInfo &info = *mCodecInfos[startIndex];
-
+        ALOGD("%s %s", __FUNCTION__, info.getCodecName());
         if (info.isEncoder() != encoder) {
+            ALOGD("%s isEncoder:%d == encoder:%d",
+                    __FUNCTION__, info.isEncoder(), encoder);
             continue;
         }
         sp<MediaCodecInfo::Capabilities> capabilities = info.getCapabilitiesFor(type);

调试文件的文件在 log\mediacodeclist.log

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

推荐阅读更多精彩内容