webrtc視頻jitterbuffer原理機(jī)制(一)

從jitterbuffer取出frame声畏,解碼

在ViEChannel類中創(chuàng)建解碼線程,在VCMReceiver類中調(diào)用jitterbuffer取出frame姻成。

bool ViEChannel::ChannelDecodeProcess()
->int32_t Decode(uint16_t maxWaitTimeMs)
->int32_t VideoReceiver::Decode(uint16_t maxWaitTimeMs)
->VCMEncodedFrame* VCMReceiver::FrameForDecoding
```
先檢查是否存在完整的幀插龄,存在則取出,若不存在科展,則檢查不完整的幀均牢,滿足條件則取出,不滿足則不取出才睹。
```
  bool found_frame = jitter_buffer_.NextCompleteTimestamp(
      max_wait_time_ms, &frame_timestamp);
  if (!found_frame)
    found_frame = jitter_buffer_.NextMaybeIncompleteTimestamp(&frame_timestamp);
```
在上一步取出frame_timestamp徘跪,再用frame_timestamp從jitterbuffer中取出幀數(shù)據(jù)
```
VCMEncodedFrame* frame = jitter_buffer_.ExtractAndSetDecode(frame_timestamp);
```
##接收到包,將包插入jitterbuffer
接收到包砂竖,將包插入jitterbuffer代碼流程
```
void UdpTransportImpl::IncomingRTPCallback
->void UdpTransportImpl::IncomingRTPFunction
->void VideoChannelTransport::IncomingRTPPacket
->int ViENetworkImpl::ReceivedRTPPacket
->int32_t ViEChannel::ReceivedRTPPacket
->int ViEReceiver::ReceivedRTPPacket
->int ViEReceiver::InsertRTPPacket
->bool ViEReceiver::ReceivePacket
->bool RtpReceiverImpl::IncomingRtpPacket
->int32_t RTPReceiverVideo::ParseRtpPacket
->int32_t ViEReceiver::OnReceivedPayloadData
->int32_t IncomingPacket
->int32_t VideoReceiver::IncomingPacket
->int32_t VCMReceiver::InsertPacket
->VCMFrameBufferEnum VCMJitterBuffer::InsertPacket
```

##decodable_frames_真椿,incomplete_frames_,free_frames_
decodable_frames_乎澄,incomplete_frames_突硝,free_frames_的處理,主要在VCMFrameBuffer類中置济,其中包含的VCMSessionInfo _sessionInfo;為主要處理成員解恰。

對于每一次收到的包,根據(jù)時(shí)間戳找到浙于,當(dāng)前幀在哪個(gè)隊(duì)列中护盈,在decodable_frames_或incomplete_frames_隊(duì)列中,若不存在羞酗,則從free_frames_隊(duì)列中給出一個(gè)空幀腐宋。
```  
VCMFrameBuffer* frame;
  FrameList* frame_list;
  const VCMFrameBufferEnum error = GetFrame(packet, &frame, &frame_list);```
```
// Gets frame to use for this timestamp. If no match, get empty frame.
VCMFrameBufferEnum VCMJitterBuffer::GetFrame(const VCMPacket& packet,
                                             VCMFrameBuffer** frame,
                                             FrameList** frame_list) {
  *frame = incomplete_frames_.PopFrame(packet.timestamp);
  if (*frame != NULL) {
    *frame_list = &incomplete_frames_;
    return kNoError;
  }
  *frame = decodable_frames_.PopFrame(packet.timestamp);
  if (*frame != NULL) {
    *frame_list = &decodable_frames_;
    return kNoError;
  }

  *frame_list = NULL;
  // No match, return empty frame.
  *frame = GetEmptyFrame();
  if (*frame == NULL) {
    // No free frame! Try to reclaim some...
    LOG(LS_WARNING) << "Unable to get empty frame; Recycling.";
    bool found_key_frame = RecycleFramesUntilKeyFrame();
    *frame = GetEmptyFrame();
    assert(*frame);
    if (!found_key_frame) {
      free_frames_.push_back(*frame);
      return kFlushIndicator;
    }
  }
  (*frame)->Reset();
  return kNoError;
}```

對于VCMSessionInfo中  complete_和decodable_的判定,每一次插入一個(gè)包,都要進(jìn)行UpdateCompleteSession();對于幀的完整性進(jìn)行檢查胸竞。
```
  size_t returnLength = InsertBuffer(frame_buffer, packet_list_it);
  UpdateCompleteSession();
  if (decode_error_mode == kWithErrors)
    decodable_ = true;
  else if (decode_error_mode == kSelectiveErrors)
    UpdateDecodableSession(frame_data);
  return static_cast<int>(returnLength);
```
UpdateCompleteSession();檢查是否有第一個(gè)包和最后一個(gè)包欺嗤,并且都是按序的,即沒有丟包卫枝,若滿足這些條件煎饼,則判定為complete_ = true;
```
void VCMSessionInfo::UpdateCompleteSession() {
  if (HaveFirstPacket() && HaveLastPacket()) {
    // Do we have all the packets in this session?
    bool complete_session = true;
    PacketIterator it = packets_.begin();
    PacketIterator prev_it = it;
    ++it;
    for (; it != packets_.end(); ++it) {
      if (!InSequence(it, prev_it)) {
        complete_session = false;
        break;
      }
      prev_it = it;
    }
    complete_ = complete_session;
  }
}
```
下面則是對于decodable_ 的判定,如果decode_error_mode 為kWithErrors模式校赤,則有一個(gè)包decodable_ 即可以為ture吆玖。如果decode_error_mode 為kSelectiveErrors,則根據(jù)rtt,幀類型马篮,已經(jīng)有的包數(shù)等條件來綜合得出decodable_ 沾乘。
具體條件則為:rtt<100,或者為關(guān)鍵幀浑测,或者已經(jīng)收到的包數(shù)在0.2\*rolling_average_packets_per_frame和0.8\*rolling_average_packets_per_frame之間時(shí)意鲸,則decodable_ 不去改變,否則為true尽爆。rolling_average_packets_per_frame為平均每幀所含的包數(shù)待笑。
```
void VCMSessionInfo::UpdateDecodableSession(const FrameData& frame_data) {
  // Irrelevant if session is already complete or decodable
  if (complete_ || decodable_)
    return;
  // TODO(agalusza): Account for bursty loss.
  // TODO(agalusza): Refine these values to better approximate optimal ones.
  // Do not decode frames if the RTT is lower than this.
  const int64_t kRttThreshold = 100;
  // Do not decode frames if the number of packets is between these two
  // thresholds.
  const float kLowPacketPercentageThreshold = 0.2f;
  const float kHighPacketPercentageThreshold = 0.8f;
  if (frame_data.rtt_ms < kRttThreshold
      || frame_type_ == kVideoFrameKey
      || !HaveFirstPacket()
      || (NumPackets() <= kHighPacketPercentageThreshold
                          * frame_data.rolling_average_packets_per_frame
          && NumPackets() > kLowPacketPercentageThreshold
                            * frame_data.rolling_average_packets_per_frame))
    return;

  decodable_ = true;
}
```
```
PS:
1惠呼、如果rolling_average_packets_per_frame>5,
kLowPacketPercentageThreshold * frame_data.rolling_average_packets_per_frame>1
那么來一個(gè)包就判定  decodable_ = true;這個(gè)是否合理?
2妓笙、 rtt很小的時(shí)候夭委,不判定為true幅狮,是希望能夠complete_ ,至于kRttThreshold = 100是否合理株灸,有待驗(yàn)證崇摄。
3、可見慌烧,關(guān)鍵幀都是完整的逐抑,不完整,不會 設(shè)置decodable_ = true;則一直為kIncomplete屹蚊。
```

對于VCMFrameBuffer中的kCompleteSession和kDecodableSession分別對應(yīng)VCMSessionInfo中的complete_ 和decodable_ 厕氨。若既不是complete_ ,也不是decodable_汹粤,則對應(yīng)kIncomplete命斧。
```
    if (_sessionInfo.complete()) {
      SetState(kStateComplete);
      return kCompleteSession;
    } else if (_sessionInfo.decodable()) {
      SetState(kStateDecodable);
      return kDecodableSession;
    }
    return kIncomplete;
```

對于上面提到的decode_error_mode,通過如下流程進(jìn)行設(shè)置嘱兼。
```
int Conductor::VideoCreateStream
->int ViEBaseImpl::CreateChannel
->int ViEBaseImpl::CreateChannel
->int ViEChannelManager::CreateChannel
->bool ChannelGroup::CreateSendChannel
->bool ChannelGroup::CreateChannel
->int32_t ViEChannel::Init
->int32_t SetVideoProtection
->int32_t VideoReceiver::SetVideoProtection
->void VCMReceiver::SetDecodeErrorMode
->void VCMJitterBuffer::SetDecodeErrorMode
```
這里根據(jù)NACK和FEC的使用情況国葬,來設(shè)置decode_error_mode。
```
// Enable or disable a video protection method.
// Note: This API should be deprecated, as it does not offer a distinction
// between the protection method and decoding with or without errors. If such a
// behavior is desired, use the following API: SetReceiverRobustnessMode.
int32_t VideoReceiver::SetVideoProtection(VCMVideoProtection videoProtection,
                                          bool enable) {
  // By default, do not decode with errors.
  _receiver.SetDecodeErrorMode(kNoErrors);
  switch (videoProtection) {
    case kProtectionNack:
    case kProtectionNackReceiver: {
      CriticalSectionScoped cs(_receiveCritSect);
      if (enable) {
        // Enable NACK and always wait for retransmits.
        _receiver.SetNackMode(kNack, -1, -1);
      } else {
        _receiver.SetNackMode(kNoNack, -1, -1);
      }
      break;
    }

    case kProtectionKeyOnLoss: {
      CriticalSectionScoped cs(_receiveCritSect);
      if (enable) {
        _keyRequestMode = kKeyOnLoss;
        _receiver.SetDecodeErrorMode(kWithErrors);
      } else if (_keyRequestMode == kKeyOnLoss) {
        _keyRequestMode = kKeyOnError;  // default mode
      } else {
        return VCM_PARAMETER_ERROR;
      }
      break;
    }

    case kProtectionKeyOnKeyLoss: {
      CriticalSectionScoped cs(_receiveCritSect);
      if (enable) {
        _keyRequestMode = kKeyOnKeyLoss;
      } else if (_keyRequestMode == kKeyOnKeyLoss) {
        _keyRequestMode = kKeyOnError;  // default mode
      } else {
        return VCM_PARAMETER_ERROR;
      }
      break;
    }

    case kProtectionNackFEC: {
      CriticalSectionScoped cs(_receiveCritSect);
      if (enable) {
        // Enable hybrid NACK/FEC. Always wait for retransmissions
        // and don't add extra delay when RTT is above
        // kLowRttNackMs.
        _receiver.SetNackMode(kNack, media_optimization::kLowRttNackMs, -1);
        _receiver.SetDecodeErrorMode(kNoErrors);
        _receiver.SetDecodeErrorMode(kNoErrors);
      } else {
        _receiver.SetNackMode(kNoNack, -1, -1);
      }
      break;
    }
    case kProtectionNackSender:
    case kProtectionFEC:
    case kProtectionPeriodicKeyFrames:
      // Ignore encoder modes.
      return VCM_OK;
  }
  return VCM_OK;
}
```


下面則介紹本文的核心,decodable_frames_汇四,incomplete_frames_接奈,free_frames_的處理。
```
  // Is the frame already in the decodable list?
  bool continuous = IsContinuous(*frame);
  switch (buffer_state) {
    case kGeneralError:
    case kTimeStampError:
    case kSizeError: {
      free_frames_.push_back(frame);
      break;
    }
    case kCompleteSession: {
      if (previous_state != kStateDecodable &&
          previous_state != kStateComplete) {
        CountFrame(*frame);
        if (continuous) {
          // Signal that we have a complete session.
          frame_event_->Set();
        }
      }
      FALLTHROUGH();
    }
    // Note: There is no break here - continuing to kDecodableSession.
    case kDecodableSession: {
      *retransmitted = (frame->GetNackCount() > 0);
      if (continuous) {
        decodable_frames_.InsertFrame(frame);
        FindAndInsertContinuousFrames(*frame);
      } else {
        incomplete_frames_.InsertFrame(frame);
      }
      break;
    }
    case kIncomplete: {
      if (frame->GetState() == kStateEmpty &&
          last_decoded_state_.UpdateEmptyFrame(frame)) {
        free_frames_.push_back(frame);
        return kNoError;
      } else {
        incomplete_frames_.InsertFrame(frame);
      }
      break;
    }
    case kNoError:
    case kOutOfBoundsPacket:
    case kDuplicatePacket: {
      // Put back the frame where it came from.
      if (frame_list != NULL) {
        frame_list->InsertFrame(frame);
      } else {
        free_frames_.push_back(frame);
      }
      ++num_duplicated_packets_;
      break;
    }
    case kFlushIndicator:
      free_frames_.push_back(frame);
      return kFlushIndicator;
    default: assert(false);
  }
```
其中第一句 bool continuous = IsContinuous(*frame);主要是判斷當(dāng)前收到幀和上一個(gè)解碼幀是不是連續(xù)的船殉。其中還有一些特殊情況鲫趁,如decode_error_mode_ == kWithErrors,或者frame->FrameType() == kVideoFrameKey等均判斷為連續(xù)的利虫。由于不加FEC和NACK時(shí)挨厚,decode_error_mode_ = kWithErrors,所以糠惫,一直continuous 為true疫剃。
只有kCompleteSession時(shí),才觸發(fā)事件frame_event_->Set();等待事件在
bool VCMJitterBuffer::NextCompleteTimestamp中硼讽,即取幀的函數(shù)中巢价。
注意:kCompleteSession情況,后面沒有break,則將完成的幀也插入decodable_frames_隊(duì)列固阁。
所以壤躲,對于VCMSessionInfo中的complete_ 和decodable_ ,都將插入decodable_frames_隊(duì)列备燃。

##再回頭看從jitterbuffer取出frame
```
// Returns immediately or a |max_wait_time_ms| ms event hang waiting for a
// complete frame, |max_wait_time_ms| decided by caller.
bool VCMJitterBuffer::NextCompleteTimestamp(
    uint32_t max_wait_time_ms, uint32_t* timestamp) {
  crit_sect_->Enter();
  if (!running_) {
    crit_sect_->Leave();
    return false;
  }
  CleanUpOldOrEmptyFrames();

  if (decodable_frames_.empty() ||
      decodable_frames_.Front()->GetState() != kStateComplete) 
  {
    const int64_t end_wait_time_ms = clock_->TimeInMilliseconds() +
        max_wait_time_ms;
    int64_t wait_time_ms = max_wait_time_ms;
    while (wait_time_ms > 0) {
      crit_sect_->Leave();
      const EventTypeWrapper ret =
        frame_event_->Wait(static_cast<uint32_t>(wait_time_ms));
      crit_sect_->Enter();
      if (ret == kEventSignaled) {
        // Are we shutting down the jitter buffer?
        if (!running_) {
          crit_sect_->Leave();
          return false;
        }
        // Finding oldest frame ready for decoder.
        CleanUpOldOrEmptyFrames();
        if (decodable_frames_.empty() ||
            decodable_frames_.Front()->GetState() != kStateComplete) {
          wait_time_ms = end_wait_time_ms - clock_->TimeInMilliseconds();
        } else {
          break;
        }
      } else {
        break;
      }
    }
  }
  if (decodable_frames_.empty() ||
      decodable_frames_.Front()->GetState() != kStateComplete) {
    crit_sect_->Leave();
    return false;
  }
  *timestamp = decodable_frames_.Front()->TimeStamp();
  crit_sect_->Leave();
  return true;
}
```
1碉克、其中decodable_frames_.Front()->GetState() 取得的_state,有下列情況賦值:
```
        if (packet.frameType != kFrameEmpty) {
            // first media packet
            SetState(kStateIncomplete);
        }
```
```
    if (_sessionInfo.complete()) {
      SetState(kStateComplete);
      return kCompleteSession;
    } else if (_sessionInfo.decodable()) {
      SetState(kStateDecodable);
      return kDecodableSession;
    }
```
可見并齐,_state還是可以標(biāo)記這一幀數(shù)據(jù)的完成情況的漏麦,即完成時(shí),為kStateComplete况褪;未完成時(shí)撕贞,為kStateDecodable或kStateIncomplete,只有一個(gè)包時(shí)测垛,為kStateIncomplete捏膨。凡是沒有從kStateIncomplete升為kStateDecodable,則依然為kStateIncomplete食侮。

2脊奋、其中條件decodable_frames_.Front()->GetState() != kStateComplete
可見,必須是完整的幀才能夠從NextCompleteTimestamp函數(shù)中取出來疙描。

再看取出不完整的幀:

```
bool VCMJitterBuffer::NextMaybeIncompleteTimestamp(uint32_t* timestamp) {
  CriticalSectionScoped cs(crit_sect_);
  if (!running_) {
    return false;
  }
  if (decode_error_mode_ == kNoErrors) {
    // No point to continue, as we are not decoding with errors.
    return false;
  }

  CleanUpOldOrEmptyFrames();

  VCMFrameBuffer* oldest_frame;
  if (decodable_frames_.empty()) {
    if (nack_mode_ != kNoNack || incomplete_frames_.size() <= 1) {
      return false;
    }
    oldest_frame = incomplete_frames_.Front();
    // Frame will only be removed from buffer if it is complete (or decodable).
    if (oldest_frame->GetState() < kStateComplete) {
      return false;
    }
  } else {
    oldest_frame = decodable_frames_.Front();
    // If we have exactly one frame in the buffer, release it only if it is
    // complete. We know decodable_frames_ is  not empty due to the previous
    // check.
    if (decodable_frames_.size() == 1 && incomplete_frames_.empty() &&
        oldest_frame->GetState() != kStateComplete) {
      return false;
    }
  }

  *timestamp = oldest_frame->TimeStamp();
  return true;
}
```
從條件中可以看出:
1诚隙、decodable_frames_為空時(shí)
incomplete_frames_.Front()->GetState()為  kStateEmpty或者kStateIncomplete,則取不出幀起胰,否則可以取出久又。
2巫延、decodable_frames_不空時(shí)
decodable_frames_.size() == 1 && incomplete_frames_.empty() &&
        oldest_frame->GetState() != kStateComplete時(shí),取不出幀地消。否則取出decodable_frames_中不完整的數(shù)據(jù)幀炉峰。

##總結(jié):
至此,jitterbuffer對于包脉执、幀的處理疼阔,已經(jīng)比較清晰。
所有組包的幀都存在于decodable_frames_半夷,incomplete_frames_隊(duì)列中婆廊,而decodable_frames_又根據(jù)狀態(tài)分為完整的幀和不完整的幀,incomplete_frames_主要保存狀態(tài)為kIncomplete的幀巫橄,也是不完整的淘邻,但有包數(shù)據(jù)。
而在取幀數(shù)據(jù)的時(shí)候先取完整的幀湘换,取不到宾舅,則取一定條件下不完整的幀數(shù)據(jù),不是有不完整的幀數(shù)據(jù)就去取彩倚。
所以筹我,如果你不想取出丟包的幀,則只調(diào)用NextCompleteTimestamp去取完整的幀即可帆离。因?yàn)槊看蝸淼陌槔#湃霂兄螅际遣迦氲絛ecodable_frames_或者incomplete_frames_的最前面盯质,所以不存在一直取不出來幀數(shù)據(jù)的情況。另外一個(gè)方法概而,就是設(shè)置decode_error_mode_ 為kNoErrors呼巷。
但是,不取出丟包的幀 赎瑰,不等于不存在馬賽克王悍。因?yàn)閬G幀,所以解碼的時(shí)候餐曼,可能會存在參考幀的問題压储。

##追加--如何杜絕馬賽克
由上述對于馬賽克存在的討論中可知,丟包是主要原因源譬,如果只取出完整的幀集惋,也會因?yàn)閰⒖紟瑔栴},導(dǎo)致馬賽克踩娘。
如果刮刑,丟包以后,就把整個(gè)GOP都刪除,直到下一個(gè)關(guān)鍵幀雷绢,同時(shí)泛烙,發(fā)現(xiàn)丟包就去請求關(guān)鍵幀,這樣就完全可以杜絕馬賽克了翘紊。但是這樣會導(dǎo)致在丟包時(shí)蔽氨,流暢度更加不好。
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末帆疟,一起剝皮案震驚了整個(gè)濱河市鹉究,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌鸯匹,老刑警劉巖坊饶,帶你破解...
    沈念sama閱讀 217,406評論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異殴蓬,居然都是意外死亡匿级,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,732評論 3 393
  • 文/潘曉璐 我一進(jìn)店門染厅,熙熙樓的掌柜王于貴愁眉苦臉地迎上來痘绎,“玉大人,你說我怎么就攤上這事肖粮」乱常” “怎么了?”我有些...
    開封第一講書人閱讀 163,711評論 0 353
  • 文/不壞的土叔 我叫張陵涩馆,是天一觀的道長行施。 經(jīng)常有香客問我,道長魂那,這世上最難降的妖魔是什么蛾号? 我笑而不...
    開封第一講書人閱讀 58,380評論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮涯雅,結(jié)果婚禮上鲜结,老公的妹妹穿的比我還像新娘。我一直安慰自己活逆,他們只是感情好精刷,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,432評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著蔗候,像睡著了一般怒允。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上锈遥,一...
    開封第一講書人閱讀 51,301評論 1 301
  • 那天误算,我揣著相機(jī)與錄音仰美,去河邊找鬼。 笑死儿礼,一個(gè)胖子當(dāng)著我的面吹牛咖杂,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播蚊夫,決...
    沈念sama閱讀 40,145評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼诉字,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了知纷?” 一聲冷哼從身側(cè)響起壤圃,我...
    開封第一講書人閱讀 39,008評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎琅轧,沒想到半個(gè)月后伍绳,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,443評論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡乍桂,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,649評論 3 334
  • 正文 我和宋清朗相戀三年冲杀,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片睹酌。...
    茶點(diǎn)故事閱讀 39,795評論 1 347
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡权谁,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出憋沿,到底是詐尸還是另有隱情旺芽,我是刑警寧澤,帶...
    沈念sama閱讀 35,501評論 5 345
  • 正文 年R本政府宣布辐啄,位于F島的核電站采章,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏壶辜。R本人自食惡果不足惜悯舟,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,119評論 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望士复。 院中可真熱鬧,春花似錦翩活、人聲如沸阱洪。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,731評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽冗荸。三九已至,卻和暖如春利耍,著一層夾襖步出監(jiān)牢的瞬間蚌本,已是汗流浹背盔粹。 一陣腳步聲響...
    開封第一講書人閱讀 32,865評論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留程癌,地道東北人舷嗡。 一個(gè)月前我還...
    沈念sama閱讀 47,899評論 2 370
  • 正文 我出身青樓,卻偏偏與公主長得像嵌莉,于是被迫代替她去往敵國和親进萄。 傳聞我的和親對象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,724評論 2 354

推薦閱讀更多精彩內(nèi)容

  • 前言 如果網(wǎng)絡(luò)是理想的锐峭,即無丟包中鼠,無抖動,低延時(shí)沿癞,那么接收到一幀完整數(shù)據(jù)就直接播放援雇,效果也一定會非常好。但是實(shí)際的...
    ai___believe閱讀 18,285評論 15 31
  • 教程一:視頻截圖(Tutorial 01: Making Screencaps) 首先我們需要了解視頻文件的一些基...
    90后的思維閱讀 4,697評論 0 3
  • [TOC] 音視頻&流媒體 是什么促使我要寫這一篇音視頻入門文章椎扬?那是因?yàn)楹鸵幻米哟蛸€碼率的概念惫搏,結(jié)果輸了;對一個(gè)...
    AllenWu閱讀 4,827評論 1 24
  • 一直希望有款app盗舰,可以發(fā)表類似日志的雜記晶府,但又不希望被好友看到,微博推薦了這款軟件钻趋,不知道好不好用川陆。 就想寫點(diǎn)小...
    大大大大壯閱讀 114評論 0 0
  • 或許较沪,他想過,自己會是臨安城中落第的秀才失仁,因功名無望尸曼,卻又身無盤纏,無力也無臉還鄉(xiāng)萄焦,只得暫且在京城落腳控轿。白天,委身...
    如此多情閱讀 530評論 0 8