前言
TS 全稱是 MPEG-2 Transport Stream嘴办,即MPEG-2標(biāo)準(zhǔn)中的傳輸流季率。TS流廣泛用于廣播電視系統(tǒng)茸塞,比如說數(shù)字電視躲庄,以及IPTV。廣播電視系統(tǒng)中钾虐,TS流如果發(fā)送過來后噪窘,就會解封裝和解碼,然后由屏幕渲染播放效扫。這里就有一個問題倔监,我們看電視有很多頻道,比如CCTV菌仁、地方衛(wèi)視等丐枉。而同一個頻道還有很多節(jié)目,就像CCTV頻道下面掘托,在同一時刻就有CCTV1-CCTV14這些節(jié)目瘦锹,那么這些頻道、節(jié)目闪盔、音視頻碼流又是如何在TS里面進行區(qū)分呢弯院?又是如何支持隨機播放呢?又是怎么完成音畫同步呢泪掀?這就是TS復(fù)雜的原因听绳。在互聯(lián)網(wǎng)中只是借用了這種封裝,只是傳了一路視頻和音頻异赫,比如直播椅挣,是一種比較簡單的場景。
PS和TS
MPEG2-PS (Program Stream) 用來存儲固定時長的視頻塔拳,例如DVD
MPEG2-TS 一般用來存儲實時的傳送的節(jié)目鼠证。
PS 流 (Program Stream):節(jié)目流,PS 流由 PS 包組成靠抑,而一個 PS 包又由若干個 PES 包組成量九。一個 PS 包由具有同一時間基準(zhǔn)的一個或多個 PES 包復(fù)合合成。
TS 流 (Transport Stream):傳輸流,TS 流由固定長度(一般為188 字節(jié))的 TS 包組成荠列,TS 包是對 PES 包的另一種封裝方式类浪,同樣由具有同一時間基準(zhǔn)的一個或多個 PES 包復(fù)合合成。PS 包是不固定長度肌似,而 TS 包為固定長度费就。
(1)188 bytes:MPEG-2標(biāo)準(zhǔn)(本文會基于MPEG-2的標(biāo)準(zhǔn)去討論ts packet)
(2)192 bytes:188 bytes + 4 bytes時間碼 --> 日本DVH-S標(biāo)準(zhǔn)
(3)204 bytes:188 bytes + 16 bytes前向糾錯碼(FEC) --> 美國ATSC標(biāo)準(zhǔn)
(4)208 bytes:188 bytes + 4 bytes時間碼 + 16 bytes前向糾錯碼(FEC)
TS格式構(gòu)成
TS文件(碼流)可以分為三層:TS層(Transport Stream)、PES層(Packet Elemental Stream)川队、ES層(Elementary Stream)力细。
ES層 :音視頻數(shù)據(jù);
PES層 : 是在音視頻數(shù)據(jù)上加了時間戳等對數(shù)據(jù)幀的說明信息呼寸;
TS層:是在PES層上加入了數(shù)據(jù)流識別和傳輸?shù)谋匾畔ⅰ?/p>
兩個特殊的TS包
PAT:Program Association Table 節(jié)目關(guān)聯(lián)表艳汽,每個 TS 流對應(yīng)一張,用來描述該 TS 流中有多少個節(jié)目对雪。
TS 流中中河狐,PAT 包重復(fù)實現(xiàn),大約 0.5 秒出現(xiàn)一個瑟捣,保證實時解碼性
表示 PAT 表的 TS 包 PID 值為 0馋艺,便于識別
PAT 的 payload 中傳送特殊 PID 的列表,每個 PID 對應(yīng)一個節(jié)目( PMT 表)
PMT:Program Map Table迈套,節(jié)目映射表捐祠,該表的 PID 是由 PAT 表 提供給出的。
表征一路節(jié)目所有流信息桑李。包含:
當(dāng)前節(jié)目中包含的所有 Video 數(shù)據(jù)的 PID
當(dāng)前節(jié)目中包含的所有 Audio 數(shù)據(jù)的 PID
與當(dāng)前節(jié)目關(guān)聯(lián)在一起的其他數(shù)據(jù)的 PID(如數(shù)字廣播踱蛀,數(shù)據(jù)通訊等使用的 PID)
TS流解析流程
1、找到PAT贵白,獲取PMT的PID
通過sync_byte=0x47找到ts packet的起始位置率拒,通過ts header中PID為0x0000找到PAT,也就是ts payload中table_id為0x00的TS包禁荒,讀取PMT的PID(program_map_PID)猬膨。
2、找到PMT呛伴,獲取流的PID
通過sync_byte=0x47找到ts packet的起始位置勃痴,通過ts header中PID在0x0010~0x1FFE中(不固定)確認(rèn)是PMT,也就是ts payload中table_id為0x02的TS包热康,讀取流類型(stream_type)及攜帶該類型流的ts packet的PID(elementary_PID)沛申。此時就找到的音頻流的PID和視頻流的PID,流PID都存儲于ts header中的PID字段褐隆。
3污它、獲取音視頻數(shù)據(jù)
根據(jù)ts header中的PID可以判斷出ts payload攜帶的是音頻還是視頻,通過ts header中的有效載荷單元起始符(payload_unit_start_indicator)庶弃,可以判斷出ts packet攜帶的PES是否是一個PES包的第一個分包衫贬。如果是PES包的第一個分包,先要找到PES包頭歇攻,提取時間戳固惯,再跳至ES數(shù)據(jù),這就是一幀數(shù)據(jù)的開始部分缴守。
4葬毫、組包
ts header中的有效載荷單元起始符(payload_unit_start_indicator)為1時,就知道這是下一幀的開始了屡穗,將前面的所有ES數(shù)據(jù)組合成一幀數(shù)據(jù)贴捡。開始下一輪組幀。
相關(guān)說明:
1村砂、ts packet是一般188 bytes烂斋,ts header是4 bytes,ts payload是0~184 bytes础废,adaptation field也是0~184 bytes汛骂。ts payload、adaptation field都是有可能不存在评腺。
2帘瞭、當(dāng)一幀數(shù)據(jù)大于188 bytes時,會被拆分成多個ts packet存儲蒿讥,一般這一幀的第一個和最后一個ts packet中存在adaptation field蝶念。第一個ts packet中的adaptation field包含了時鐘參考(PCR_flag=1),而最后一個ts packet中的adaptation field是為了填充ts packet使之達到188 bytes芋绸,此時的adaptation field中沒有時鐘參考(PCR_flag=0)媒殉。
3、封裝時一個PES包封裝的是一幀數(shù)據(jù)侥钳,由于一個PES包大于188 bytes适袜,在存儲時一個PES包往往存儲于整數(shù)個ts packet中。但是只有第一個ts packet含有pes packet header舷夺,后面的ts packet只有es數(shù)據(jù)苦酱。并不是所有攜帶音視頻數(shù)據(jù)的ts packet都含有pes packet header。
4给猾、ts header中的有效載荷單元起始符(payload_unit_start_indicator)來判斷疫萤,payload_unit_start_indicator=1時,就知道這是下一幀的開始了敢伸,將前面的所有ES數(shù)據(jù)組合成一幀數(shù)據(jù)扯饶。然后開始下一輪組幀。
5、一個ts文件中PAT尾序、PMT是多組的钓丰,不僅僅只有文件開始處才有。從文件中間播放時可以找到的就近的PAT每币、PMT來找到音視頻流携丁。
6、TS碼流由于采用了固定長度的包結(jié)構(gòu)兰怠,當(dāng)傳輸誤碼破壞了某一TS包的同步信息時梦鉴,接收機可在固定的位置檢測它后面包中的同步信息,從而恢復(fù)同步揭保,避免了信息丟失肥橙。因此,在信道環(huán)境較為惡劣秸侣,傳輸誤碼較高時存筏,一般采用TS碼流。由于TS碼流具有較強的抵抗傳輸誤碼的能力塔次,因此目前在傳輸媒體中進行傳輸?shù)腗PEG-2碼流基本上都采用了TS碼流的包格式方篮。
7、由于ts packet大小固定為188 bytes励负,當(dāng)數(shù)據(jù)不足188 bytes時藕溅,會有調(diào)整字段,此時增加了封裝數(shù)據(jù)的大小继榆。使得文件大小變大巾表。PAT、PMT循環(huán)插入在音視頻數(shù)據(jù)中略吨,也增加了封裝數(shù)據(jù)集币。
8、TS流中不包含快速seek的機制翠忠,只能通過協(xié)議層實現(xiàn)seek鞠苟。HLS協(xié)議基于TS流實現(xiàn)的。
如果是單獨的TS秽之,是如何實現(xiàn)時長統(tǒng)計和seek邏輯的当娱?下面我們以EXO代碼為例進行分析
代碼分析:
TS時長計算:
@Override
public @ReadResult int read(ExtractorInput input, PositionHolder seekPosition)
throws IOException {
long inputLength = input.getLength();
if (tracksEnded) { //已經(jīng)解析了PMT
boolean canReadDuration = inputLength != C.LENGTH_UNSET && mode != MODE_HLS;
//在讀取其他數(shù)據(jù)時優(yōu)先獲取時長
if (canReadDuration && !durationReader.isDurationReadFinished()) {
return durationReader.readDuration(input, seekPosition, pcrPid);
}
maybeOutputSeekMap(inputLength);
}
}
public @Extractor.ReadResult int readDuration(
ExtractorInput input, PositionHolder seekPositionHolder, int pcrPid) throws IOException {
if (pcrPid <= 0) {
return finishReadDuration(input);
}
if (!isLastPcrValueRead) {
//讀取最后的PCR數(shù)據(jù)
return readLastPcrValue(input, seekPositionHolder, pcrPid);
}
if (lastPcrValue == C.TIME_UNSET) {
return finishReadDuration(input);
}
if (!isFirstPcrValueRead) {
//讀取第一個PCR數(shù)據(jù)
return readFirstPcrValue(input, seekPositionHolder, pcrPid);
}
if (firstPcrValue == C.TIME_UNSET) {
return finishReadDuration(input);
}
long minPcrPositionUs = pcrTimestampAdjuster.adjustTsTimestamp(firstPcrValue);
long maxPcrPositionUs = pcrTimestampAdjuster.adjustTsTimestamp(lastPcrValue);
//兩個PCR數(shù)據(jù)后轉(zhuǎn)換相減,得到時長
durationUs = maxPcrPositionUs - minPcrPositionUs;
if (durationUs < 0) {
Log.w(TAG, "Invalid duration: " + durationUs + ". Using TIME_UNSET instead.");
durationUs = C.TIME_UNSET;
}
return finishReadDuration(input);
}
private int readLastPcrValue(ExtractorInput input, PositionHolder seekPositionHolder, int pcrPid)
throws IOException {
long inputLength = input.getLength();
//timestampSearchBytes = 600 * 188 默認(rèn)值
int bytesToSearch = (int) min(timestampSearchBytes, inputLength);
long searchStartPosition = inputLength - bytesToSearch;
if (input.getPosition() != searchStartPosition) {
seekPositionHolder.position = searchStartPosition;
return Extractor.RESULT_SEEK;
}
packetBuffer.reset(bytesToSearch);
input.resetPeekPosition();
input.peekFully(packetBuffer.getData(), /* offset= */ 0, bytesToSearch);
lastPcrValue = readLastPcrValueFromBuffer(packetBuffer, pcrPid);
isLastPcrValueRead = true;
return Extractor.RESULT_CONTINUE;
}
時長計算說明:
時長 = lastpcr - firstpcr考榨。
lastpcr 必須從末尾獲取跨细,pcr不一定存在哪個位置,從末尾讀取數(shù)據(jù)少了河质,無法獲取到pcr冀惭;數(shù)據(jù)讀取多了耗時較久震叙。EXO默認(rèn)讀取600*188字節(jié)的數(shù)據(jù)。所以經(jīng)常會解析不到時長散休。
此處我們可以進行優(yōu)化媒楼,文件大小為length, 先從(length - 600188) 開始讀取 600188 字節(jié),如果不能獲取PCR數(shù)據(jù)溃槐,則繼續(xù)(length - 600188 * N)開始讀取 600188 + 188字節(jié)(此處+188字節(jié)匣砖,主要考慮上次讀取的位置不是以0x47開頭)科吭,直到獲取到PCR位置昏滴。
從實現(xiàn)看,視頻播放至少發(fā)起三次網(wǎng)絡(luò)請求对人,并且從末尾讀取 600 * 188字節(jié)谣殊,所以首幀比較慢。如果是斷點續(xù)播牺弄,則請求次數(shù)還要增加姻几,啟播速度更慢。
seek邏輯:
public int handlePendingSeek(ExtractorInput input, PositionHolder seekPositionHolder)
throws IOException {
while (true) {
SeekOperationParams seekOperationParams =
Assertions.checkStateNotNull(this.seekOperationParams);
long floorPosition = seekOperationParams.getFloorBytePosition();
long ceilingPosition = seekOperationParams.getCeilingBytePosition();
long searchPosition = seekOperationParams.getNextSearchBytePosition();
if (ceilingPosition - floorPosition <= minimumSearchRange) {
// The seeking range is too small, so we can just continue from the floor position.
markSeekOperationFinished(/* foundTargetFrame= */ false, floorPosition);
return seekToPosition(input, floorPosition, seekPositionHolder);
}
if (!skipInputUntilPosition(input, searchPosition)) {
return seekToPosition(input, searchPosition, seekPositionHolder);
}
input.resetPeekPosition();
TimestampSearchResult timestampSearchResult =
timestampSeeker.searchForTimestamp(input, seekOperationParams.getTargetTimePosition());
switch (timestampSearchResult.type) {
case TimestampSearchResult.TYPE_POSITION_OVERESTIMATED:
seekOperationParams.updateSeekCeiling(
timestampSearchResult.timestampToUpdate, timestampSearchResult.bytePositionToUpdate);
break;
case TimestampSearchResult.TYPE_POSITION_UNDERESTIMATED:
seekOperationParams.updateSeekFloor(
timestampSearchResult.timestampToUpdate, timestampSearchResult.bytePositionToUpdate);
break;
case TimestampSearchResult.TYPE_TARGET_TIMESTAMP_FOUND:
skipInputUntilPosition(input, timestampSearchResult.bytePositionToUpdate);
markSeekOperationFinished(
/* foundTargetFrame= */ true, timestampSearchResult.bytePositionToUpdate);
return seekToPosition(
input, timestampSearchResult.bytePositionToUpdate, seekPositionHolder);
case TimestampSearchResult.TYPE_NO_TIMESTAMP:
// We can't find any timestamp in the search range from the search position.
// Give up, and just continue reading from the last search position in this case.
markSeekOperationFinished(/* foundTargetFrame= */ false, searchPosition);
return seekToPosition(input, searchPosition, seekPositionHolder);
default:
throw new IllegalStateException("Invalid case");
}
}
}
public TimestampSearchResult searchForTimestamp(ExtractorInput input, long targetTimestamp)
throws IOException {
long inputPosition = input.getPosition();
int bytesToSearch = (int) min(timestampSearchBytes, input.getLength() - inputPosition);
packetBuffer.reset(bytesToSearch);
input.peekFully(packetBuffer.getData(), /* offset= */ 0, bytesToSearch);
return searchForPcrValueInBuffer(packetBuffer, targetTimestamp, inputPosition);
}
private TimestampSearchResult searchForPcrValueInBuffer(
ParsableByteArray packetBuffer, long targetPcrTimeUs, long bufferStartOffset) {
int limit = packetBuffer.limit();
long startOfLastPacketPosition = C.INDEX_UNSET;
long endOfLastPacketPosition = C.INDEX_UNSET;
long lastPcrTimeUsInRange = C.TIME_UNSET;
while (packetBuffer.bytesLeft() >= TsExtractor.TS_PACKET_SIZE) {
int startOfPacket =
TsUtil.findSyncBytePosition(packetBuffer.getData(), packetBuffer.getPosition(), limit);
int endOfPacket = startOfPacket + TsExtractor.TS_PACKET_SIZE;
if (endOfPacket > limit) {
break;
}
long pcrValue = TsUtil.readPcrFromPacket(packetBuffer, startOfPacket, pcrPid);
if (pcrValue != C.TIME_UNSET) {
long pcrTimeUs = pcrTimestampAdjuster.adjustTsTimestamp(pcrValue);
if (pcrTimeUs > targetPcrTimeUs) {
if (lastPcrTimeUsInRange == C.TIME_UNSET) {
// First PCR timestamp is already over target.
return TimestampSearchResult.overestimatedResult(pcrTimeUs, bufferStartOffset);
} else {
// Last PCR timestamp < target timestamp < this timestamp.
return TimestampSearchResult.targetFoundResult(
bufferStartOffset + startOfLastPacketPosition);
}
} else if (pcrTimeUs + SEEK_TOLERANCE_US > targetPcrTimeUs) {
long startOfPacketInStream = bufferStartOffset + startOfPacket;
return TimestampSearchResult.targetFoundResult(startOfPacketInStream);
}
lastPcrTimeUsInRange = pcrTimeUs;
startOfLastPacketPosition = startOfPacket;
}
packetBuffer.setPosition(endOfPacket);
endOfLastPacketPosition = endOfPacket;
}
if (lastPcrTimeUsInRange != C.TIME_UNSET) {
long endOfLastPacketPositionInStream = bufferStartOffset + endOfLastPacketPosition;
return TimestampSearchResult.underestimatedResult(
lastPcrTimeUsInRange, endOfLastPacketPositionInStream);
} else {
return TimestampSearchResult.NO_TIMESTAMP_IN_RANGE_RESULT;
}
}
通過查找PCR势告,找到合適的seek位置(通過碼率估算位置)蛇捌。
此處存在兩個問題
Exo查找600188,如果找不到咱台,則不進行查找(此處可以繼續(xù)讀取600188络拌,直到找到PCR數(shù)據(jù)位置)。
查找到seek位置后回溺,并不不知道關(guān)鍵幀的位置春贸。所以此處可能造成聲音播放,畫面不動的問題遗遵。直到遇到第一個關(guān)鍵幀后播放恢復(fù)正常萍恕。
參考連接:
https://blog.csdn.net/m0_60259116/article/details/125207225
https://blog.csdn.net/weixin_39399492/article/details/129019329
(2)推薦TS的分析工具:Elecard Stream Analyzer