WebRTC实现简单音视频通话功能

1 WebRTC音视频通话功能简介

本文介绍如何基于WebRTC快速实现一个简单的实时音视频通话。

在开始之前,您可以先了解一些实时音视频推拉流相关的基础概念:

  • 流:一组按指定编码格式封装的音视频数据内容。一个流可以包含几个轨道,比如视频和音频轨道。
  • 推流:把采集阶段封包好的音视频数据流推送到 ZEGO 实时音视频云的过程。
  • 拉流:从 ZEGO 实时音视频云将已有音视频数据流拉取播放的过程。
  • 房间:是 ZEGO 提供的音视频空间服务,用于组织用户群,同一房间内的用户可以互相收发实时音视频及消息。
    1. 用户需要先登录某个房间,才能进行音视频推流、拉流操作。
    2. 用户只能收到自己所在房间内的相关消息(用户进出、音视频流变化等)。

更多相关概念可参考即构官网关于音视频SDK的介绍 术语说明

2 实现WebRTC视频通话的前提条件

在实现基本的WebRTC实时音视频功能之前,请确保:

3 WebRTC音视频通话示例代码

我们提供了一个实现了WebRTC音视频通话基本流程的完整示例 HTML 文件,可作为WebRTC开发过程中的参考。

<!DOCTYPE html>
<html>

<head>
    <meta charset="UTF-8">
    <title>Zego Express Video Call</title>
    <!-- 此处需要改成正确的 SDK 版本号 -->
    <script src="ZegoExpressWebRTC-x.x.x.js"></script>

    <style type="text/css">
        h1,
        h4 {
            text-align: center;
        }

        .video-wrapper {
            width: 610px;
            margin: 0 auto;
        }

        .video-wrapper h4 {
            width: 300px;
            display: inline-block;
            position: relative;
        }
        #remote-video, #local-video {
            width: 300px;
            height: 270px;
            display: inline-block;
            position: relative;
        }

        .video-wrapper video {
            height: auto;
        }
    </style>
</head>

<body>
    <h1>
        Zego RTC Video Call
    </h1>
    <div class="video-wrapper">
        <h4>Local video</h4>
        <h4>Remote video</h4>
        <div id="local-video"></div>
        <div id="remote-video"></div>
    </div>
    <script>
        // 文档中的 js 示例代码可粘贴至此处
        // 项目唯一标识 AppID,Number 类型,请从 ZEGO 控制台获取
        let appID = 0
        // 接入服务器地址 Server,String 类型,请从 ZEGO 控制台获取(获取方式请参考上文“前提条件”)
        let server = ""

        // 初始化实例
        const zg = new ZegoExpressEngine(appID, server);
        zg.setDebugVerbose(false)
        // 房间状态更新回调
        // 此处在登录房间成功后,立即进行推流。在实现具体业务时,您可选择其他时机进行推流,只要保证当前房间连接状态是连接成功的即可。
        // 房间状态更新回调
        zg.on('roomStateChanged', async (roomID, reason, errorCode, extendedData) => {
            if (reason == 'LOGINED') {
                console.log("与房间连接成功,只有当房间状态是连接成功时,才能进行推流、拉流等操作。")
            }
        })

        zg.on('roomUserUpdate', (roomID, updateType, userList) => {
            // 其他用户进出房间的通知
        });

        zg.on('roomStreamUpdate', async (roomID, updateType, streamList, extendedData) => {
            // 房间内其他用户音视频流变化的通知
            if (updateType == 'ADD') {
                // 流新增,开始拉流
                // 此处演示拉取流新增的列表中第一条流的音视频
                const streamID = streamList[0].streamID;
                // streamList 中有对应流的 streamID
                const remoteStream = await zg.startPlayingStream(streamID);
                // 创建媒体流播放组件
                const remoteView = zg.createRemoteStreamView(remoteStream);
                remoteView.play("remote-video", {enableAutoplayDialog:true});

            } else if (updateType == 'DELETE') {
                // 流删除,通过流删除列表 streamList 中每个流的 streamID 进行停止拉流。
                const streamID = streamList[0].streamID;
                zg.stopPlayingStream(streamID)
            }
        });

        // 登录房间,成功则返回 true
        // userUpdate 设置为 true 才能收到 roomUserUpdate 回调。
        let userID = "user1"; // userID 用户自己设置,必须保证全局唯一
        let userName = "user1";// userName 用户自己设置,没有唯一性要求
        let roomID = "123"; // roomID 用户自己设置,必须保证全局唯一
        // token 由用户自己的服务端生成,为了更快跑通流程,可以通过即构控制台 https://console.zego.im/ 获取临时的音视频 token,token 为字符串
        let token = ``;

        zg.loginRoom(roomID, token, { userID, userName: userID }, { userUpdate: true }).then(async result => {
            if (result == true) {
                console.log("login success");
                // 与房间连接成功,只有当房间状态是连接成功时,才能进行推流、拉流等操作。
                // 创建流、预览
                // 调用 createStream 接口后,需要等待 ZEGO 服务器返回流媒体对象才能执行后续操作
                const localStream = await zg.createStream();
                // 创建媒体流播放组件
                const localView = zg.createLocalStreamView(localStream);
                localView.play("local-video", {enableAutoplayDialog:true});
                // 开始推流,将自己的音视频流推送到 ZEGO 音视频云,此处 streamID 由用户定义,需全局唯一
                let streamID = new Date().getTime().toString();
                zg.startPublishingStream(streamID, localStream)
            }
        });
        // // 登录房间的第二种写法
        // (async function main(){
        //     await zg.loginRoom(roomID, token, { userID, userName: userID }, { userUpdate: true })
        // })()
    </script>
</body>

</html>

4 WebRTC音视频通话实现流程

以用户 A 拉取用户 B 的流为例,一次简单的WebRTC实时音视频通话主要流程如下:

  1. 用户 A 创建实例,登录房间。(登录成功后,可预览自己的画面并推流。)
  2. 用户 B 创建实例,登录同一个房间。登录成功后,用户 B 开始推流,此时 SDK 会触发 roomStreamUpdate 回调,表示房间内有流的变化。
  3. 用户 A 可通过监听 roomStreamUpdate 回调,当回调通知有流新增时,获取用户 B 的流 ID,来拉取播放用户 B 刚刚推送的流。
image.png

4.1 创建WebRTC实时音视频通话界面

为方便实现基本的WebRTC实时音视频功能,您可参考WebRTC实时音视频的示例代码和下图实现一个简单实时音视频功能的页面。

image.png

打开或新建 “index.html” 页面文件,并拷贝以下代码到文件中。

<html>

<head>
    <meta charset="UTF-8">
    <title>Zego Express Video Call</title>
    <style type="text/css">
        * {
            font-family: sans-serif;
        }

        h1,
        h4 {
            text-align: center;
        }

        #local-video, #remote-video {
            width: 400px;
            height: 300px;
            border: 1px solid #dfdfdf;
        }

        #local-video {
            position: relative;
            margin: 0 auto;
            display: block;
        }

        #remote-video {
            display: flex;
            margin: auto;
            position: relative !important;
        }
    </style>
</head>

<body>
    <h1>
        Zego RTC Video Call
    </h1>
    <h4>Local video</h4>
    <div id="local-video"></div>
    <h4>Remote video</h4>
    <div id="remote-video"></div>
    <script>
    // 文档中的 js 示例代码可粘贴至此处
    // const zg = new ZegoExpressEngine(appID, server);
    </script>
</body>

</html>

4.2 创建引擎并监听回调

  1. 创建并初始化一个 ZegoExpressEngine 的实例,将您项目的 AppID 传入参数 “appID”,Server 传入参数 “server”。

  2. 即构实时音视频SDK 提供如房间连接状态、音视频流变化、用户进出等通知回调。为避免错过任何通知,您需要在创建 ZegoExpressEngine 后立即监听回调。

// 项目唯一标识 AppID,Number 类型,请从 ZEGO 控制台获取
let appID = ; 
// 接入服务器地址 Server,String 类型,请从 ZEGO 控制台获取(获取方式请参考上文“前提条件”)
let server = "";

// 初始化实例
const zg = new ZegoExpressEngine(appID, server);

// 房间状态更新回调
zg.on('roomStateChanged', (roomID, reason, errorCode, extendData) => {
        if (reason == 'LOGINING') {
            // 登录中
        } else if (reason == 'LOGINED') {
            // 登录成功
            //只有当房间状态是登录成功或重连成功时,推流(startPublishingStream)、拉流(startPlayingStream)才能正常收发音视频
            //将自己的音视频流推送到 ZEGO 音视频云
        } else if (reason == 'LOGIN_FAILED') {
            // 登录失败
        } else if (reason == 'RECONNECTING') {
            // 重连中
        } else if (reason == 'RECONNECTED') {
            // 重连成功
        } else if (reason == 'RECONNECT_FAILED') {
            // 重连失败
        } else if (reason == 'KICKOUT') {
            // 被踢出房间
        } else if (reason == 'LOGOUT') {
            // 登出成功
        } else if (reason == 'LOGOUT_FAILED') {
            // 登出失败
        }
});

//房间内其他用户进出房间的通知
//只有调用 loginRoom 登录房间时传入 ZegoRoomConfig,且 ZegoRoomConfig 的 userUpdate 参数为 “true” 时,用户才能收到 roomUserUpdate回调。
zg.on('roomUserUpdate', (roomID, updateType, userList) => {
    if (updateType == 'ADD') {
        for (var i = 0; i < userList.length; i++) {
            console.log(userList[i]['userID'], '加入了房间:', roomID)
        }
    } else if (updateType == 'DELETE') {
        for (var i = 0; i < userList.length; i++) {
            console.log(userList[i]['userID'], '退出了房间:', roomID)
        }
    }
});

zg.on('roomStreamUpdate', async (roomID, updateType, streamList, extendedData) => {
    // 房间内其他用户音视频流变化的通知
});

4.3 检测浏览器WebRTC兼容性

考虑到不同的浏览器对 WebRTC 的兼容性不同,在实现实时音视频推拉流功能之前,您需要检测浏览器能否正常运行 WebRTC。

您可以调用 checkSystemRequirements 接口检测浏览器的兼容性,检测结果的含义,请参考 ZegoCapabilityDetection 接口下的参数描述。

const result = await zg.checkSystemRequirements();
// 返回的 result 为兼容性检测结果。 webRTC 为 true 时表示支持 webRTC,其他属性含义可以参考接口 API 文档。
console.log(result);
// {
//   webRTC: true,
//   customCapture: true,
//   camera: true,
//   microphone: true,
//   videoCodec: { H264: true, H265: false, VP8: true, VP9: true },
//   screenSharing: true,
//   errInfo: {}
// }

您还可以通过 ZEGO 提供的实时音视频推拉流在线检测工具 在线检测工具,在需要检测的浏览器中打开,直接检测浏览器的兼容性。请参考 浏览器兼容性说明 获取 音视频SDK 支持的浏览器兼容版本。

4.4 登录房间

1. 生成 Token

登录房间需要用于验证身份的 Token,开发者可直接在 ZEGO 控制台获取临时 Token(有效期为 24 小时)来使用,详情请参考 控制台 - 项目管理 中的 “项目信息”。

临时 Token 仅供调试,正式上线前,请从开发者的业务服务器生成 Token,详情可参考 使用 Token 鉴权

2. 登录房间

调用 loginRoom 接口,传入房间 ID 参数 “roomID”、“token” 和用户参数 “user”,根据实际情况传入参数 “config”,登录房间。

  • “roomID”、“userID” 和 “userName” 参数的取值都为自定义。
  • “roomID” 和 “userID” 都必须唯一,建议开发者将 “userID” 设置为一个有意义的值,可将其与自己的业务账号系统进行关联。
  • 只有调用 loginRoom 接口登录房间时传入 ZegoRoomConfig 配置,且 “userUpdate” 参数取值为 “true” 时,用户才能收到 roomUserUpdate 回调。
// 登录房间,成功则返回 true
// userUpdate 设置为 true 才能收到 roomUserUpdate 回调。

let userID = Util.getBrow() + '_' + new Date().getTime();
let userName = "user0001";
let roomID = "0001";
let token = ;
// 为避免错过任何通知,您需要在登录房间前先监听用户加入/退出房间、房间连接状态变更、推流状态变更等回调。
zg.on('roomStateChanged', async (roomID, reason, errorCode, extendedData) => {

})
zg.loginRoom(roomID, token, { userID, userName: userID }, { userUpdate: true }).then(result => {
     if (result == true) {
        console.log("login success")
     }
});

您可以监听 roomStateChanged 回调实时监控自己与房间的连接状态。只有当房间状态是连接成功时,才能进行推流、拉流等操作。

4.5 预览自己的画面,并推送到 ZEGO 音视频云

  1. 创建流并预览自己的画面

开始推流前需要创建本端的音视频流,调用 createStream 接口获取媒体流对象,默认会采集摄像头画面和麦克风声音。媒体流对象可以使用 createLocalStreamView 创建本地媒体流播放组件进行播放,也可以通过 video 元素 srcObject 属性赋值进行播放。

  1. 需等待 createStream 接口返回流媒体对象后,再将自己的音视频流推送到 ZEGO 音视频云。

调用 startPublishingStream 接口,传入 “streamID” 和创建流得到的流对象 “localStream”,向远端用户发送本端的音视频流。

“streamID” 由您本地生成,但是需要保证同一个 AppID 下,“streamID” 全局唯一。如果同一个 AppID 下,不同用户各推了一条 “streamID” 相同的流,会导致后推流的用户推流失败。

// 此处在登录房间成功后,立即进行推流。在实现具体业务时,您可选择其他时机进行推流,只要保证当前房间连接状态是连接成功的即可。

zg.loginRoom(roomID, token, { userID, userName: userID }, { userUpdate: true }).then(async result => {
     if (result == true) {
        console.log("login success")
        // 创建流、预览
           // 调用 createStream 接口后,需要等待 ZEGO 服务器返回流媒体对象才能执行后续操作
           const localStream = await zg.createStream();
           
           // 创建媒体流播放组件对象,用于预览本地流
           const localView = zg.createLocalStreamView(localStream);
           // 将播放组件挂载到页面,"local-video" 为组件容器 DOM 元素的 id 。
           localView.play("local-video");

           // 开始推流,将自己的音视频流推送到 ZEGO 音视频云
           let streamID = new Date().getTime().toString();
           zg.startPublishingStream(streamID, localStream)
     }
});

(可选)设置音视频采集参数

通过属性设置相关采集参数

可根据需要通过 createStream 接口中的如下属性设置音视频相关采集参数,详情可参考 自定义视频采集

  • camera:摄像头麦克风采集流相关配置

  • screen:屏幕捕捉采集流相关配置

  • custom:第三方流采集相关配置

4.6 拉取其他用户的音视频

进行视频通话时,我们需要拉取到其他用户的音视频。

房间内有其他用户加入时,SDK 会触发 roomStreamUpdate 回调,通知房间内有流新增,基于此可获取其他用户的 “streamID”。此时,调用 startPlayingStream 接口根据传入的其他用户的 “streamID”,拉取远端已推送到 ZEGO 服务器的音视频画面。若需要从 CDN 拉流,可参考 使用 CDN 直播

// 流状态更新回调
zg.on('roomStreamUpdate', async (roomID, updateType, streamList, extendedData) => {
    // 当 updateType 为 ADD 时,代表有音视频流新增,此时可以调用 startPlayingStream 接口拉取播放该音视频流
    if (updateType == 'ADD') {
        // 流新增,开始拉流
        // 这里为了使示例代码更加简洁,我们只拉取新增的音视频流列表中第的第一条流,在实际的业务中,建议开发者循环遍历 streamList ,拉取每一条音视频流 
        const streamID = streamList[0].streamID;
        // streamList 中有对应流的 streamID
        const remoteStream = await zg.startPlayingStream(streamID);

        // 创建媒体流播放组件对象,用于播放远端媒体流 。
        const remoteView = zg.createRemoteStreamView(remoteStream);
        // 将播放组件挂载到页面,"remote-video" 为组件容器 DOM 元素的 id 。
        remoteView.play("remote-video");

    } else if (updateType == 'DELETE') {
        // 流删除,停止拉流
    }
});
  • 部分浏览器因自动播放限制策略问题,使用 ZegoStreamView 媒体流播放组件进行播放媒体流可能受阻,SDK 默认会在界面上弹窗提示恢复播放。
  • 您可以将 ZegoStreamView.play() 方法的第二个参数 options.enableAutoplayDialog 设置为 false 关闭自动弹窗,通过在 autoplayFailed 事件回调中,在页面上显示一个按钮,引导用户点击恢复播放。

至此,您已经成功实现了简单的实时音视频通话,可在浏览器中打开 "index.html",体验实时音视频功能。

5 实时音视频SDK常用功能

5.1 常用通知回调

// 房间连接状态更新回调
// 本地调用 loginRoom 加入房间时,您可通过监听 roomStateChanged 回调实时监控自己在本房间内的连接状态。
zg.on('roomStateChanged', (roomID, reason, errorCode, extendData) => {
    if (reason == 'LOGINING') {
        // 登录中
    } else if (reason == 'LOGINED') {
        // 登录成功
        //只有当房间状态是登录成功或重连成功时,推流(startPublishingStream)、拉流(startPlayingStream)才能正常收发音视频
        //将自己的音视频流推送到 ZEGO 音视频云
    } else if (reason == 'LOGIN_FAILED') {
        // 登录失败
    } else if (reason == 'RECONNECTING') {
        // 重连中
    } else if (reason == 'RECONNECTED') {
        // 重连成功
    } else if (reason == 'RECONNECT_FAILED') {
        // 重连失败
    } else if (reason == 'KICKOUT') {
        // 被踢出房间
    } else if (reason == 'LOGOUT') {
        // 登出成功
    } else if (reason == 'LOGOUT_FAILED') {
        // 登出失败
    }
});

//房间内其他用户推送的音视频流新增/减少的通知
//自己推送的流不能在这里接收到通知
zg.on('roomStreamUpdate', async (roomID, updateType, streamList, extendedData) => {
    if (updateType == 'ADD') {
        // 流新增
        for (var i = 0; i < streamList.length; i++) {
            console.log('房间',roomID,'内新增了流:', streamList[i]['streamID'])
        }
        const message = "其他用户的视频流streamID: " + streamID.toString();
    } else if (updateType == 'DELETE') {
        // 流删除
        for (var i = 0; i < streamList.length; i++) {
            console.log('房间',roomID,'内减少了流:', streamList[i]['streamID'])
        }
    }
});

//房间内其他用户进出房间的通知
//只有调用 loginRoom 登录房间时传入 ZegoRoomConfig,且 ZegoRoomConfig 的 userUpdate 参数为 “true” 时,用户才能收到 roomUserUpdate回调。
zg.on('roomUserUpdate', (roomID, updateType, userList) => {
    if (updateType == 'ADD') {
        for (var i = 0; i < userList.length; i++) {
            console.log(userList[i]['userID'], '加入了房间:', roomID)
        }
    } else if (updateType == 'DELETE') {
        for (var i = 0; i < userList.length; i++) {
            console.log(userList[i]['userID'], '退出了房间:', roomID)
        }
    }
});

//用户推送音视频流的状态通知
//用户推送音视频流的状态发生变更时,会收到该回调。如果网络中断导致推流异常,SDK 在重试推流的同时也会通知状态变化。
zg.on('publisherStateUpdate', result => {
    // 推流状态更新回调
    var state = result['state']
    var streamID = result['streamID']
    var errorCode = result['errorCode']
    var extendedData = result['extendedData']
    if (state == 'PUBLISHING') {
        console.log('成功推送音视频流:', streamID);
    } else if (state == 'NO_PUBLISH') {
        console.log('未推送音视频流');
    } else if (state == 'PUBLISH_REQUESTING') {
        console.log('请求推送音视频流:', streamID);
    }
    console.log('错误码:', errorCode,' 额外信息:', extendedData)
})

//推流质量回调。
//成功推流后,您会定时收到回调音视频流质量数据(如分辨率、帧率、码率等)。
zg.on('publishQualityUpdate', (streamID, stats) => {
    // 推流质量回调
    console.log('流质量回调')
})

//用户拉取音视频流的状态通知
//用户拉取音视频流的状态发生变更时,会收到该回调。如果网络中断导致拉流异常,SDK 会自动进行重试。
zg.on('playerStateUpdate', result => {
    // 拉流状态更新回调
    var state = result['state']
    var streamID = result['streamID']
    var errorCode = result['errorCode']
    var extendedData = result['extendedData']
    if (state == 'PLAYING') {
        console.log('成功拉取音视频流:', streamID);
    } else if (state == 'NO_PLAY') {
        console.log('未拉取音视频流');
    } else if (state == 'PLAY_REQUESTING') {
        console.log('请求拉取音视频流:', streamID);
    }
    console.log('错误码:', errorCode,' 额外信息:', extendedData)
})

//拉取音视频流时的质量回调。
//成功拉流后,您会定时收到拉取音视频流时的质量数据通知(如分辨率、帧率、码率等)。
zg.on('playQualityUpdate', (streamID,stats) => {
    // 拉流质量回调
})

//收到广播消息的通知
zg.on('IMRecvBroadcastMessage', (roomID, chatData) => {
    console.log('广播消息IMRecvBroadcastMessage', roomID, chatData[0].message);
    alert(chatData[0].message)
});

//收到弹幕消息的通知
zg.on('IMRecvBarrageMessage', (roomID, chatData) => {
    console.log('弹幕消息IMRecvBroadcastMessage', roomID, chatData[0].message);
    alert(chatData[0].message)
});

//收到自定义信令消息的通知
zg.on('IMRecvCustomCommand', (roomID, fromUser, command) => {
    console.log('自定义消息IMRecvCustomCommand', roomID, fromUser, command);
    alert(command)
});

5.2 停止音视频通话

1. 停止推流、销毁流

调用 stopPublishingStream 接口停止向远端用户发送本端的音视频流。调用 destroyStream 接口销毁创建的流数据,销毁流后开发需自行销毁 video(停止采集)。

// 根据本端 streamID 停止推流
zg.stopPublishingStream(streamID)
// localStream 是调用 createStream 接口获取的 MediaStream 对象
zg.destroyStream(localStream)

2. 停止拉流

调用 stopPlayingStream 接口停止拉取远端推送的音视频流。

// 流状态更新回调
zg.on('roomStreamUpdate', async (roomID, updateType, streamList, extendedData) => {
    if (updateType == 'ADD') {
        // 流新增,开始拉流
    } else if (updateType == 'DELETE') {
        // 流删除,通过流删除列表 streamList 中每个流的 streamID 进行停止拉流。
        const streamID = streamList[0].streamID;
        zg.stopPlayingStream(streamID)
    }
});

3. 退出房间

调用 logoutRoom 接口退出房间。

zg.logoutRoom(roomID)

以上整个推拉流过程的 API 调用时序可参考下图:


image.png

6 调试视频通话功能

在真机中运行项目,运行成功后,可以看到本端视频画面。

为方便体验,ZEGO 提供了一个 Web 端调试示例 ,在该页面下,输入相同的 AppID、RoomID,输入一个不同的 UserID,即可加入同一房间与真机设备互通。当成功开始音视频通话时,可以听到远端的音频,看到远端的视频画面。

7 获取音视频SDK更多支持

获取本文的Demo、开发文档、技术支持,访问即构文档中心

近期有开发规划的开发者可上即构官网查看,恰逢即构七周年全线音视频产品1折的优惠,联系商务获取RTC产品优惠;

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

推荐阅读更多精彩内容