Janus(三)VideoRoom 文档

VideoRoom plugin documentation

房间配置文件

room-<unique room ID>: {
        description = This is my awesome room
        is_private = true|false (private rooms don't appear when you do a 'list' request)
        secret = <optional password needed for manipulating (e.g. destroying) the room>
        pin = <optional password needed for joining the room>
        require_pvtid = true|false (whether subscriptions are required to provide a valid
                                 a valid private_id to associate with a publisher, default=false)
        publishers = <max number of concurrent senders> (e.g., 6 for a video
                                 conference or 1 for a webinar, default=3)
        bitrate = <max video bitrate for senders> (e.g., 128000)
        fir_freq = <send a FIR to publishers every fir_freq seconds> (0=disable)
        audiocodec = opus|g722|pcmu|pcma|isac32|isac16 (audio codec to force on publishers, default=opus
                                can be a comma separated list in order of preference, e.g., opus,pcmu)
        videocodec = vp8|vp9|h264 (video codec to force on publishers, default=vp8
                                can be a comma separated list in order of preference, e.g., vp9,vp8,h264)
        opus_fec = true|false (whether inband FEC must be negotiated; only works for Opus, default=false)
        video_svc = true|false (whether SVC support must be enabled; only works for VP9, default=false)
        audiolevel_ext = true|false (whether the ssrc-audio-level RTP extension must be
                negotiated/used or not for new publishers, default=true)
        audiolevel_event = true|false (whether to emit event to other users or not)
        audio_active_packets = 100 (number of packets with audio level, default=100, 2 seconds)
        audio_level_average = 25 (average value of audio level, 127=muted, 0='too loud', default=25)
        videoorient_ext = true|false (whether the video-orientation RTP extension must be
                negotiated/used or not for new publishers, default=true)
        playoutdelay_ext = true|false (whether the playout-delay RTP extension must be
                negotiated/used or not for new publishers, default=true)
        transport_wide_cc_ext = true|false (whether the transport wide CC RTP extension must be
                negotiated/used or not for new publishers, default=true)
        record = true|false (whether this room should be recorded, default=false)
        rec_dir = <folder where recordings should be stored, when enabled>
        notify_joining = true|false (optional, whether to notify all participants when a new
                                participant joins the room. The Videoroom plugin by design only notifies
                                new feeds (publishers), and enabling this may result extra notification
                                traffic. This flag is particularly useful when enabled with \c require_pvtid
                                for admin to manage listening only participants. default=false)
}

API 分类

  • 同步请求:直接在事务(transacation)的上下文里获取响应,即直接响应(transaction绑定即可)
    • create:动态创建一个新的房间,作为使用配置文件的替代方法
    • destroy:移除并销毁房间,同时剔除房间里的所有用户
    • edit:动态修改一些房间属性
    • exists:查询是否存在指定的房间
    • list:列出所有可用的房间
    • allowed:配置观众进入房间权限
    • kick:管理员踢除用户操作
    • listparticipants:列出所有指定房间中活跃(比如当前正在发布)的参与者和他们的详细信息
  • 异步请求:在事件(event)通知中获取成功(success)与失败(failure)事件,先发一个ask后再发一个event进行响应(必须使用handle_id绑定)
    • join:加入指定的房间,并指定连接用于发布或观看音视频
    • joinandconfigure:对于发布者,将前两个请求合成一个
    • configure:修改一些参与者的设置,例如 bitrate cap
    • publish:开始发送媒体并广播给其他参与者
    • unpublish:停止发送媒体并广播给其他参与者
    • start:开始接收来自发布者的媒体数据
    • pause:停止传递媒体数据
    • switch:修改媒体连接的来源
    • leave:离开房间

Video Room

create - 创建房间

请求用 body

{
        "request" : "create",
        "room" : <unique numeric ID, optional, chosen by plugin if missing>,
        "permanent" : <true|false, whether the room should be saved in the config file, default=false>,
        "description" : "<pretty name of the room, optional>",
        "secret" : "<password required to edit/destroy the room, optional>",
        "pin" : "<password required to join the room, optional>",
        "is_private" : <true|false, whether the room should appear in a list request>,
        "allowed" : [ array of string tokens users can use to join this room, optional],
        ...
}

更多房间创建参数可参考房间配置文件——conf/janus.plugin.videoroom.jcfg

成功响应 created

{
        "videoroom" : "created",
        "room" : <unique numeric ID>,
        "permanent" : <true if saved to config file, false if not>
}

(通用)失败回调 event

{
        "videoroom" : "event",
        "error_code" : <numeric ID, check Macros below>,
        "error" : "<error description as a string>"
}

Janus 中所有用户都可以创建房间,我们可以在插件中配置admin_key,只有在create携带了正确的admin_key才可以创建成功。

注意,您也可以选择将此功能扩展到RTP转发,以便只允许受信任的客户端使用此功能。

edit - 编辑房间

在房间被创建后,我们可以使用edit编辑房间的属性(例如room descriptionsecretpinprivate or not),但不能修改一些静态的属性(例如room IDsampling rate)。

请求用 body

{
        "request" : "edit",
        "room" : <unique numeric ID of the room to edit>,
        "secret" : "<room secret, mandatory if configured>",
        "new_description" : "<new pretty name of the room, optional>",
        "new_secret" : "<new password required to edit/destroy the room, optional>",
        "new_pin" : "<new password required to join the room, optional>",
        "new_is_private" : <true|false, whether the room should appear in a list request>,
        "new_require_pvtid" : <true|false, whether the room should require private_id from subscribers>,
        "new_bitrate" : <new bitrate cap to force on all publishers (except those with custom overrides)>,
        "new_fir_freq" : <new period for regular PLI keyframe requests to publishers>,
        "new_publishers" : <new cap on the number of concurrent active WebRTC publishers>,
        "permanent" : <true|false, whether the room should be also removed from the config file, default=false>
}

成功响应 edited

{
        "videoroom" : "edited",
        "room" : <unique numeric ID>
}

destroy - 销毁房间

无论是静态还是动态的房间,都可以用destroy进行销毁。

请求用 body

{
        "request" : "destroy",
        "room" : <unique numeric ID of the room to destroy>,
        "secret" : "<room secret, mandatory if configured>",
        "permanent" : <true|false, whether the room should be also removed from the config file, default=false>
}

成功响应 destoryed

{
        "videoroom" : "destroyed",
        "room" : <unique numeric ID>
}

房间用户接收的事件 event

用户可能在房间使用中销毁房间,其他观众会收到这个事件,获知房间被销毁

{
        "videoroom" : "destroyed",
        "room" : <unique numeric ID of the destroyed room>
}

exists - 房间是否存在

请求用 body

{
        "request" : "exists",
        "room" : <unique numeric ID of the room to check>
}

成功响应 - success

{
        "videoroom" : "success",
        "room" : <unique numeric ID>,
        "exists" : <true|false>
}

allowed - 出入管理

用于配置是否启用token验证,添加或移除允许进入房间的用户。

请求用 body

{
        "request" : "allowed",
        "secret" : "<room secret, mandatory if configured>",
        "action" : "enable|disable|add|remove",
        "room" : <unique numeric ID of the room to update>,
        "allowed" : [
                // Array of strings (tokens users might pass in "join", only for add|remove)
        ]
}

成功响应 success

{
        "videoroom" : "success",
        "room" : <unique numeric ID>,
        "allowed" : [
                // Updated, complete, list of allowed tokens (only for enable|add|remove)
        ]
}

kick - 踢出房间

房间的管理员可以通过kick方法将指定参与者踢出房间。注意,只是能将他踢出房间,并不能阻止他再次进入。若想阻止他再次进入,则需要使用allowed对他进行限制。

请求用 body

{
        "request" : "kick",
        "secret" : "<room secret, mandatory if configured>",
        "room" : <unique numeric ID of the room>,
        "id" : <unique numeric ID of the participant to kick>
}

成功响应 - success

{
        "videoroom" : "success",
}

list - 房间列表

用于展示公开的所有房间

请求用 body

{
        "request" : "list"
}

成功响应 - success

{
        "videoroom" : "success",
        "rooms" : [             // Array of room objects
                {       // Room #1
                        "room" : <unique numeric ID>,
                        "description" : "<Name of the room>",
                        "pin_required" : <true|false, whether a PIN is required to join this room>,
                        "max_publishers" : <how many publishers can actually publish via WebRTC at the same time>,
                        "bitrate" : <bitrate cap that should be forced (via REMB) on all publishers by default>,
                        "bitrate_cap" : <true|false, whether the above cap should act as a limit to dynamic bitrate changes by publishers>,
                        "fir_freq" : <how often a keyframe request is sent via PLI/FIR to active publishers>,
                        "audiocodec" : "<comma separated list of allowed audio codecs>",
                        "videocodec" : "<comma separated list of allowed video codecs>",
                        "record" : <true|false, whether the room is being recorded>,
                        "record_dir" : "<if recording, the path where the .mjr files are being saved>",
                        "num_participants" : <count of the participants (publishers, active or not; not subscribers)>
                },
                // Other rooms
        ]
}

listparticipants

请求用 body

{
        "request" : "listparticipants",
        "room" : <unique numeric ID of the room>
}

成功响应 participants

{
        "videoroom" : "participants",
        "room" : <unique numeric ID of the room>,
        "participants" : [              // Array of participant objects
                {       // Participant #1
                        "id" : <unique numeric ID of the participant>,
                        "display" : "<display name of the participant, if any; optional>",
                        "talking" : <true|false, whether user is talking or not (only if audio levels are used)>,
                        "internal_audio_ssrc" : <audio SSRC used internally for this active publisher>,
                        "internal_video_ssrc" : <video SSRC used internally for this active publisher>
                },
                // Other participants
        ]
}

异步请求

角色梳理

  • 发布者 Publisher
    • 活跃的发布者 active publisher
    • 不活跃的发布者 passive publisher
  • 订阅者 Subscriber

VideoRoom Publishers

在音频房间中,发布者(Publisher)是将多媒体数据发布在房间中的参与者,他们可以成为feed让其他用户进行订阅(subscribe)。

join - 加入房间

我们可以通过join请求将指定的handle指定为publishersubscriber

  • 指定为publisher:Janus 会将指定用户添加到房间中的参与者列表(the list of participants)中,此后,参与者可以接受到房间中关于handle的一系列通知(包括流的可用与移除)。
  • 指定为subscriber

请求用 body

{
        "request" : "join",
        "ptype" : "publisher",
        "room" : <unique ID of the room to join>,
        "id" : <unique ID to register for the publisher; optional, will be chosen by the plugin if missing>,
        "display" : "<display name for the publisher; optional>",
        "token" : "<invitation token, in case the room has an ACL; optional>"
}

成功响应 joined

成功join入房间后,将会接收到当前房间中活跃的publishers的列表。

{
        "videoroom" : "joined",
        "room" : <room ID>,
        "description" : <description of the room, if available>,
        "id" : <unique ID of the participant>,
        "private_id" : <a different unique ID associated to the participant; meant to be private>,
        "publishers" : [
                {
                        "id" : <unique ID of active publisher #1>,
                        "display" : "<display name of active publisher #1, if any>",
                        "audio_codec" : "<audio codec used by active publisher #1, if any>",
                        "video_codec" : "<video codec used by active publisher #1, if any>",
                        "simulcast" : "<true if the publisher uses simulcast (VP8 and H.264 only)>",
                        "talking" : <true|false, whether the publisher is talking or not (only if audio levels are used)>,
                },
                // Other active publishers
        ],
        "attendees" : [         // Only present when notify_joining is set to TRUE for rooms
                {
                        "id" : <unique ID of attendee #1>,
                        "display" : "<display name of attendee #1, if any>"
                },
                // Other attendees
        ]
}

当房间配置了notify_joiningTRUE时,也会返回不推流的参与者。

private_id:该属性用于订阅,便于插件对其进行关联。

关于不活跃的发布者进出通知

不活跃的发布者加入房间不会通知其他参与者,因为一个大房间中可能会有很多参与者,如果每一个进入离开都需要通知所有人的话会显得太过于繁琐。如果开发者它进出房间也通知所有人,那么需要将房间的notify_joining配置为true。通知的事件如下:

{
        "videoroom" : "event",
        "room" : <room ID>,
        "joining" : {
                "id" : <unique ID of the new participant>,
                "display" : "<display name of the new participant, if any>"
        }
}

publish - 发布(开始发布1)

发布者带着 Offer 向 Janus 进行协商

publish请求必须携带着 JSEP SDP offer去协商一个新的 PeerConnection,即插件会检查编码器和码率是否是房间所支持的,之后将会回应一个 JSEP SDP acswer 来关闭循环并完成配置一个 PeerConnection。当指定的 PeerConnection 搭建好后,该指定的 publisher 才会变成一个活跃的 publisher,之后其他参与者才可以订阅它。

请求用 Body

{
        "request" : "publish",
        "audio" : <true|false, depending on whether or not audio should be relayed; true by default>,
        "video" : <true|false, depending on whether or not video should be relayed; true by default>,
        "data" : <true|false, depending on whether or not data should be relayed; true by default>,
        "audiocodec" : "<audio codec to prefer among the negotiated ones; optional>",
        "videocodec" : "<video codec to prefer among the negotiated ones; optional>",
        "bitrate" : <bitrate cap to return via REMB; optional, overrides the global room value if present>,
        "record" : <true|false, whether this publisher should be recorded or not; optional>,
        "filename" : "<if recording, the base path/file to use for the recording files; optional>",
        "display" : "<new display name to use in the room; optional>"
}

成功响应 event

Janus 会在准备好 Answer 后进行响应,该 event 中也会带着 JSEP SDP answer

{
        "videoroom" : "event",
        "configured" : "ok"
}

configure - 配置(开始发布2)

publishconfigure在功能上时等价的,他们都有开始发布的功能。从语义上,publish更加合适。但是configure可以更新发布者会话的一些属性。总的来说configure的功能更加强大,用它就没错了。

请求用 body

{
        "request" : "configure",
        "audio" : <true|false, depending on whether or not audio should be relayed; true by default>,
        "video" : <true|false, depending on whether or not video should be relayed; true by default>,
        "data" : <true|false, depending on whether or not data should be relayed; true by default>,
        "bitrate" : <bitrate cap to return via REMB; optional, overrides the global room value if present (unless bitrate_cap is set)>,
        "keyframe" : <true|false, whether we should send this publisher a keyframe request>,
        "record" : <true|false, whether this publisher should be recorded or not; optional>,
        "filename" : "<if recording, the base path/file to use for the recording files; optional>",
        "display" : "<new display name to use in the room; optional>"
}

成功响应 event

{
        "videoroom" : "event",
        "configured" : "ok"
}

joinandconfigure - 加入并配置(加入并且发布)

我们可以通过使用joinandconfigure在一个请求中进行加入和发布两个操作。

请求用 body

参考join请求用的 body,并且携带 JSEP SDP offer

成功响应 - joined

成功响应将是一个joined事件,并且携带 JSEP SDP answer。

发布成功后推送

当一个用户在房间中发布成功后,Janus 将会发送一个关于新对端的事件给所有参与者,事件如下:

{
        "videoroom" : "event",
        "room" : <room ID>,
        "publishers" : [
                {
                        "id" : <unique ID of the new publisher>,
                        "display" : "<display name of the new publisher, if any>",
                        "audio_codec" : "<audio codec used the new publisher, if any>",
                        "video_codec" : "<video codec used by the new publisher, if any>",
                        "simulcast" : "<true if the publisher uses simulcast (VP8 and H.264 only)>",
                        "talking" : <true|false, whether the publisher is talking or not (only if audio levels are used)>,
                }
        ]
}

unpublish - 停止发布

unpublish请求用于停止发布并且销毁相关的 PeerConnection,并且将发布者从活跃流列表中移除。

请求用 body

{
        "request" : "unpublish"
}

成功响应 event

{
        "videoroom" : "event",
        "unpublished" : "ok"
}

其他成员通知 event

发布者停止订阅成功后,房间内的其他成员会通过下面这个通知得知此事

{
        "videoroom" : "event",
        "room" : <room ID>,
        "unpublished" : <unique ID of the publisher who unpublished>
}

我们可以在同一个 handle 中多次发布和取消发布流

VideoRoom Subscribers

在 VideoRoom 中,订阅者(Subscriber)不是参与者,但是他可以准确接收指定发布者的媒体数据。通常发布者发布了媒体后,订阅者可以获取该流进行播放。当发布者取消发布后,订阅者 handle 也会随之被移除。除非给订阅者正确的信息,不然订阅者是不能单独存在的。

join - 加入房间

请求用 body

{
        "request" : "join",
        "ptype" : "subscriber",
        "room" : <unique ID of the room to subscribe in>,
        "feed" : <unique ID of the publisher to subscribe to; mandatory>,
        "private_id" : <unique ID of the publisher that originated this request; optional, unless mandated by the room configuration>,
        "close_pc" : <true|false, depending on whether or not the PeerConnection should be automatically closed when the publisher leaves; true by default>,
        "audio" : <true|false, depending on whether or not audio should be relayed; true by default>,
        "video" : <true|false, depending on whether or not video should be relayed; true by default>,
        "data" : <true|false, depending on whether or not data should be relayed; true by default>,
        "offer_audio" : <true|false; whether or not audio should be negotiated; true by default if the publisher has audio>,
        "offer_video" : <true|false; whether or not video should be negotiated; true by default if the publisher has video>,
        "offer_data" : <true|false; whether or not datachannels should be negotiated; true by default if the publisher has datachannels>,
        "substream" : <substream to receive (0-2), in case simulcasting is enabled; optional>,
        "temporal" : <temporal layers to receive (0-2), in case simulcasting is enabled; optional>,
        "spatial_layer" : <spatial layer to receive (0-2), in case VP9-SVC is enabled; optional>,
        "temporal_layer" : <temporal layers to receive (0-2), in case VP9-SVC is enabled; optional>
}

如果发布者对房间中的媒体进行订阅,则需要传入private_id

成功响应 attached(Offer)

订阅者成功加入房间并订阅指定feed成功后,Janus 会给予一个attached响应,并且在这个event中携带 JSEP SDP offer,用于进行媒体协商。

{
        "videoroom" : "attached",
        "room" : <room ID>,
        "feed" : <publisher ID>,
        "display" : "<the display name of the publisher, if any>"
}

start - 开始订阅(Answer 对 attached 的订阅者响应)

订阅者接收到含有attachevent后,获取里面的 JSEP SDP offer 后,将其设置为远程 SDP 后,然后发送start请求并携带 JSEP SDP answer。

请求用 body

{
        "request" : "start"
}

成功响应 (event - started)

{
        "videoroom" : "event",
        "started" : "ok"
}

当订阅者与Janus媒体协商完毕后,双方就会建立一条新的 WebRTC PeerConnection。在这之后,Streaming插件将会开始将媒体转发到订阅者端。

Notice that the same exact steps we just went through (watch request, followed by JSEP offer by the plugin, followed by start request with JSEP answer by the viewer) is what you also use when renegotiations are needed, e.g., for the purpose of ICE restarts.

pause - 暂停订阅

我们可以通过pausestart请求对订阅媒体进行临时的暂停和恢复

此处的 start 不需要携带 JSEP SDP answer

请求用 body

{
        "request" : "pause"
}

成功响应(event - paused)

{
        "videoroom" : "event",
        "paused" : "ok"
}

用于恢复的start其用法和上面是一致的,注意他不用携带 JSEP 即可。

configure - 配置

configure请求允许动态修改订阅者对于媒体订阅的一些属性,比如配置音频和视频的启用与禁用,灵活设置订阅内容。

请求用 body

{
        "request" : "configure",
        "audio" : <true|false, depending on whether audio should be relayed or not; optional>,
        "video" : <true|false, depending on whether video should be relayed or not; optional>,
        "data" : <true|false, depending on whether datachannel messages should be relayed or not; optional>,
        "substream" : <substream to receive (0-2), in case simulcasting is enabled; optional>,
        "temporal" : <temporal layers to receive (0-2), in case simulcasting is enabled; optional>,
        "spatial_layer" : <spatial layer to receive (0-2), in case VP9-SVC is enabled; optional>,
        "temporal_layer" : <temporal layers to receive (0-2), in case VP9-SVC is enabled; optional>
}
  • audiovideodata:可以在媒体层(media-level)上进行暂停与恢复,对比pausestart直接作用所有音视频流而言,它更加灵活。
  • substreamtemporal: The substream and temporal properties, instead, only make sense when the mountpoint is configured with video simulcasting support, and as such the viewer is interested in receiving a specific substream or temporal layer, rather than any other of the available ones.
  • spatial_layertemporal_layer:The spatial_layer and temporal_layer have exactly the same meaning, but within the context of VP9-SVC publishers, and will have no effect on subscriptions associated to regular publishers.

switch - 切换

switch适用于已经与 Janus 成功建立 PeerConnection 的订阅者,通过这个请求完成订阅的切换,就像看电视切换频道一样。这样做的好处就是能复用一条连接,在切换订阅目标的时候不需要重新建立连接,消耗不必要的资源。但是他也有以下的限制:

  • 不能只换音视频流的其中一个,要都换
  • 两个目标发布者必须使用一样的配置,比如一样的编码器、一样的数据格式(不一样的话会导致延迟甚至直接黑屏没声音)

请求用 body

{
        "request" : "switch",
        "feed" : <unique ID of the new publisher to switch to; mandatory>,
        "audio" : <true|false, depending on whether audio should be relayed or not; optional>,
        "video" : <true|false, depending on whether video should be relayed or not; optional>,
        "data" : <true|false, depending on whether datachannel messages should be relayed or not; optional>
}

成功响应 event

{
        "videoroom" : "event",
        "switched" : "ok",
        "room" : <room ID>,
        "id" : <unique ID of the new publisher>
}

leave - 离开房间

请求用 body

leave请求用于停止订阅并销毁相关的 PeerConnection,由于上下文是隐式的,所以不需要其他参数:

{
        "request" : "leave"
}

成功响应 - left event

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