H5直播系列八 flv.js學習筆記

一、io包,拿到數據

1.load.js有BaseLoader,以下幾個loader都繼承覆蓋它:
(a)fetch-stream-loader
fetch + stream IO loader. Currently working on chrome 43+.
fetch provides a better alternative http API to XMLHttpRequest

(b)websocket-loader
For FLV over WebSocket live stream

(c)xhr-moz-chunked-loader
For FireFox browser which supports xhr.responseType = 'moz-chunked-arraybuffer'

(d)xhr-msstream-loader
For IE11/Edge browser by microsoft which supports xhr.responseType = 'ms-stream'

(e)xhr-range-loader
Universal IO Loader, implemented by adding Range header in xhr's request header

2.speed-sampler.js
Utility class to calculate realtime network I/O speed

3.seekType
解析得到Loader中要用的參數
'range' use range request to seek, or 'param' add params into url to indicate request range.
param-seek-handler.js
range-seek-handler.js

4.iocontroller.js對上面幾個類的綜合使用
this._selectSeekHandler();//根據seektype確定用哪個handler
this._selectLoader();//確定用哪個Loader
this._createLoader();

二、demux包

對FLV數據格式進行解析,參考FLV.JS 代碼解讀--demux部分
解析完了 tag header后面分別按照不同的 tag type調用不同的解析函數。

//flv-demuxer.js
switch (tagType) {
    case 8:  // Audio
        this._parseAudioData(chunk, dataOffset, dataSize, timestamp);
        break;
    case 9:  // Video
        this._parseVideoData(chunk, dataOffset, dataSize, timestamp, byteStart + offset);
        break;
    case 18:  // ScriptDataObject
        this._parseScriptData(chunk, dataOffset, dataSize);
        break;
}

1.amf-parser.js
這個是用來解析上面所說的tagType=18,即ScriptDataObject。在flv-demuxer.js中,可以看到能解析出以下屬性:hasAudio,hasVideo,autiodatarate,videodatarate,width,height,duration,framerate,keyframes

2.音頻解析
分成AAC和MP3兩種格式

3.視頻解析
exp-golomb.js和sps-parser.js
packetType有兩種,0 表示 AVCDecoderConfigurationRecord,這個是H.264的視頻信息頭,包含了 sps 和 pps,AVCDecoderConfigurationRecord的格式不是flv定義的,而是264標準定義的,如果用ffmpeg去解碼,這個結構可以直接放到 codec的extradata里送給ffmpeg去解釋。

flv.js作者選擇了自己來解析這個數據結構,也是迫不得已,因為JS環境下沒有ffmpeg,解析這個結構主要是為了提取 sps和pps。雖然理論上sps允許有多個,但其實一般就一個。let config = SPSParser.parseSPS(sps);pps的信息沒什么用,所以作者只實現了sps的分析器,說明作者下了很大功夫去學習264的標準,其中的Golomb解碼還是挺復雜的,能解對不容易,我在PC和手機平臺都是用ffmpeg去解析的。SPS里面包括了視頻分辨率,幀率,profile level等視頻重要信息。

三、core包部分類

1.features.js 檢測支持度
2.media-info.js
media數據
3.media-segment-info.js
(a)SampleInfo
Represents an media sample (audio / video)
(b)MediaSegmentInfo
// Media Segment concept is defined in Media Source Extensions spec.
// Particularly in ISO BMFF format, an Media Segment contains a moof box followed by a mdat box.
(c)IDRSampleList
// Ordered list for recording video IDR frames, sorted by originalDts
(d)MediaSegmentInfoList
// Data structure for recording information of media segments in single track.

四、flv.js流程
 if (flvjs.isSupported()) {
      var videoElement = document.getElementById('videoElement');
      var flvPlayer = flvjs.createPlayer({
          type: 'flv',
          url: 'http://example.com/flv/video.flv'
      });
      flvPlayer.attachMediaElement(videoElement);
      flvPlayer.load();
      flvPlayer.play();
  }

1.flv.js
createPlayer(mediaDataSource, optionalConfig)
2.flv-player.js
(a)constructor(mediaDataSource, config) {}
(b)attachMediaElement主要設置MSEController

attachMediaElement(mediaElement) {
this._mediaElement = mediaElement;
this._msectl = new MSEController();
this._msectl.on...
this._msectl.attachMediaElement(mediaElement);
}

(c)在load方法中,Transmuxer和MSEController建立關聯

load() {
this._transmuxer = new Transmuxer(this._mediaDataSource, this._config);
this._transmuxer.on(TransmuxingEvents.INIT_SEGMENT, (type, is) => {
    this._msectl.appendInitSegment(is);
});
this._transmuxer.on(TransmuxingEvents.MEDIA_SEGMENT, (type, ms) => {
    this._msectl.appendMediaSegment(ms);
this._transmuxer.on...
this._transmuxer.open();
}

TransmuxingEvents.INIT_SEGMENT和TransmuxingEvents.MEDIA_SEGMENT很重要。
(d)

play() {
    this._mediaElement.play();
}

這個簡單,相當于讓頁面上的video標簽調用play方法
(e)core/transmuxer.js

this._controller = new TransmuxingController(mediaDataSource, config);

(f)transmuxing-worker.js
Enable separated thread for transmuxing (unstable for now)
多線程加載,可以先忽略

(g)transmuxing-controller.js
// Transmuxing (IO, Demuxing, Remuxing) controller, with multipart support

// treat single part media as multipart media, which has only one segment
if (!mediaDataSource.segments) {
    mediaDataSource.segments = [{
        duration: mediaDataSource.duration,
        filesize: mediaDataSource.filesize,
        url: mediaDataSource.url
    }];
}
//上面(c)步驟中的this._transmuxer.open();會執行到this._controller.start();
start(){
this._loadSegment(0);
this._enableStatisticsReporter();
}

_loadSegment(segmentIndex, optionalFrom) {
    this._currentSegmentIndex = segmentIndex;
    let dataSource = this._mediaDataSource.segments[segmentIndex];

    let ioctl = this._ioctl = new IOController(dataSource, this._config, segmentIndex);
    ioctl.onError = this._onIOException.bind(this);
    ioctl.onSeeked = this._onIOSeeked.bind(this);
    ioctl.onComplete = this._onIOComplete.bind(this);
    ioctl.onRedirect = this._onIORedirect.bind(this);
    ioctl.onRecoveredEarlyEof = this._onIORecoveredEarlyEof.bind(this);

    if (optionalFrom) {
        this._demuxer.bindDataSource(this._ioctl);
    } else {
        ioctl.onDataArrival = this._onInitChunkArrival.bind(this);
    }

    ioctl.open(optionalFrom);
}

_onInitChunkArrival(data, byteStart) {
...
// Always create new FLVDemuxer
this._demuxer = new FLVDemuxer(probeData, this._config);

if (!this._remuxer) {
    this._remuxer = new MP4Remuxer(this._config);
}
this._demuxer.onError = this._onDemuxException.bind(this);
this._demuxer.onMediaInfo = this._onMediaInfo.bind(this);

this._remuxer.bindDataSource(this._demuxer
             .bindDataSource(this._ioctl
));

this._remuxer.onInitSegment = this._onRemuxerInitSegmentArrival.bind(this);
this._remuxer.onMediaSegment = this._onRemuxerMediaSegmentArrival.bind(this);

consumed = this._demuxer.parseChunks(data, byteStart);
}

_onRemuxerInitSegmentArrival(type, initSegment) {
    this._emitter.emit(TransmuxingEvents.INIT_SEGMENT, type, initSegment);
}
_onRemuxerMediaSegmentArrival(type, mediaSegment) {
    if (this._pendingSeekTime != null) {
        // Media segments after new-segment cross-seeking should be dropped.
        return;
    }
    this._emitter.emit(TransmuxingEvents.MEDIA_SEGMENT, type, mediaSegment);
...
}
this._remuxer.onInitSegment = this._onRemuxerInitSegmentArrival.bind(this);
this._remuxer.onMediaSegment = this._onRemuxerMediaSegmentArrival.bind(this);

這兩句回調很重要,對應著TransmuxingEvents.INIT_SEGMENT和TransmuxingEvents.MEDIA_SEGMENT事件。
一旦_remuxer觸發了這兩個事件,就表示告訴msectroller,我封裝好了,可以渲染顯示了。下面就去mse-controller.js中看看,然后再看this._remuxer.onInitSegmentthis._remuxer.onMediaSegment

(h)mse-controller.js
// Media Source Extensions controller
先插播一段MSE的API,參考使用 MediaSource 搭建流式播放器
在瀏覽器里,首先我們要判斷是否支持 MediaSource:
var supportMediaSource = 'MediaSource' in window
然后就可以新建一個 MediaSource 對象,并且把 mediaSource 作為 objectURL 附加到 video 標簽上上:

var mediaSource = new MediaSource()
var video = document.querySelector('video')
video.src = URL.createObjectURL(mediaSource)

接下來就可以監聽 mediaSource 上的 sourceOpen 事件,并且設置一個回調:

mediaSource.addEventListener('sourceopen', sourceOpen);
function sourceOpen {
    // todo...
}

接下來會用到一個叫 SourceBuffer 的對象,這個對象提供了一系列接口,這里用到的是 appendBuffer 方法,可以動態地向 MediaSource 中添加視頻/音頻片段(對于一個 MediaSource,可以同時存在多個 SourceBuffer)

function sourceOpen () {
    // 這個奇怪的字符串后面再解釋
    var mime = 'video/mp4; codecs="avc1.42E01E, mp4a.40.2"'

    // 新建一個 sourceBuffer
    var sourceBuffer = mediaSource.addSourceBuffer(mime);

    // 加載一段 chunk,然后 append 到 sourceBuffer 中
    fetchBuffer('/xxxx.mp4', buffer => {
        sourceBuffer.appendBuffer(buffer)
    })
}

// 以二進制格式請求某個url
function fetchBuffer (url, callback) {
    var xhr = new XMLHttpRequest;
    xhr.open('get', url);
    xhr.responseType = 'arraybuffer';
    xhr.onload = function () {
        callback(xhr.response);
    };
    xhr.send();
}

上面這些代碼基本上就是一個最簡化的流程了,加載了一段視頻 chunk,然后把它『喂』到播放器中。

可以在mse-controller.js發現addSourceBuffer和appendBuffer方法

appendInitSegment(initSegment, deferred) {
  ...
  let sb = this._sourceBuffers[is.type] = this._mediaSource.addSourceBuffer(mimeType);
  ...
}

_doAppendSegments() {
  ...
  this._sourceBuffers[type].appendBuffer(segment.data);
  ...
  this._doAppendSegments();
}

appendMediaSegment(mediaSegment) {
  ...
  this._doAppendSegments();
}

(i)mp4-remuxer.js
現在回頭來看看_remuxer的onInitSegment 和onMediaSegment 。注意看_onTrackMetadataReceived方法的最后調用了this._onInitSegment。
MP4Remuxer

bindDataSource(producer) {
    producer.onDataAvailable = this.remux.bind(this);
    producer.onTrackMetadata = this._onTrackMetadataReceived.bind(this);
    return this;
}

remux(audioTrack, videoTrack) {
    if (!this._onMediaSegment) {
        throw new IllegalStateException('
        MP4Remuxer: onMediaSegment callback must be specificed!');
    }
    if (!this._dtsBaseInited) {
        this._calculateDtsBase(audioTrack, videoTrack);
    }
    this._remuxVideo(videoTrack);
    this._remuxAudio(audioTrack);
}

_onTrackMetadataReceived(type, metadata) {
    let metabox = null;

    let container = 'mp4';
    let codec = metadata.codec;

    if (type === 'audio') {
        this._audioMeta = metadata;
        if (metadata.codec === 'mp3' && this._mp3UseMpegAudio) {
            // 'audio/mpeg' for MP3 audio track
            container = 'mpeg';
            codec = '';
            metabox = new Uint8Array();
        } else {
            // 'audio/mp4, codecs="codec"'
            metabox = MP4.generateInitSegment(metadata);
        }
    } else if (type === 'video') {
        this._videoMeta = metadata;
        metabox = MP4.generateInitSegment(metadata);
    } else {
        return;
    }

    // dispatch metabox (Initialization Segment)
    if (!this._onInitSegment) {
        throw new IllegalStateException('MP4Remuxer: onInitSegment callback must be specified!');
    }
    this._onInitSegment(type, {
        type: type,
        data: metabox.buffer,
        codec: codec,
        container: `${type}/${container}`,
        mediaDuration: metadata.duration  // in timescale 1000 (milliseconds)
    });
}

_remuxVideo(videoTrack) {
    …
    this._onMediaSegment('video', {
        type: 'video',
        data: this._mergeBoxes(moofbox, mdatbox).buffer,
        sampleCount: mp4Samples.length,
        info: info
    });
}

可以看到線索跑到了bindDataSource方法上,那么調用這個方法是在哪里呢
。這又回到了transmuxing-controller.js

_onInitChunkArrival(data, byteStart) {
...
// Always create new FLVDemuxer
this._demuxer = new FLVDemuxer(probeData, this._config);

if (!this._remuxer) {
    this._remuxer = new MP4Remuxer(this._config);
}
this._demuxer.onError = this._onDemuxException.bind(this);
this._demuxer.onMediaInfo = this._onMediaInfo.bind(this);

this._remuxer.bindDataSource(this._demuxer
             .bindDataSource(this._ioctl
));
}

_remuxer的bindDataSource方法使用了兩個回調:onDataAvailable和onTrackMetadata 。這就進一步把控制權交出去了,也就是說只要_demuxer準備好了,remuxer就會通知MSE可以渲染了。

可以在flv-demuxer.js中發現
this._onDataAvailable(this._audioTrack, this._videoTrack);,不過onTrackMetadata沒看到調用。

再看this._demuxer.bindDataSource(this._ioctl)
flv-demuxer.js

   bindDataSource(loader) {
        loader.onDataArrival = this.parseChunks.bind(this);
        return this;
    }

顯然要去io-controler.js中去找onDataArrival方法,從名字上就能猜到這是第一部分講的那幾個loader數據加載回來了。而parseChunks方法是第二部分demux包中的解析音頻,視頻,script數據。

現在把流程再執行一遍
1.HTML頁面執行flvPlayer.load();
2.跳到flv-player.js中,this._transmuxer.open();
3.跳到transmuxer.js中,this._controller.start();
4.跳到transmuxer-controller.js中,執行this._loadSegment(0);在這個方法中,進一步執行ioctl.open(optionalFrom);
5.跳到io-controller.js中,執行this._loader.open(this._dataSource, Object.assign({}, this._currentRange));
6.有很多類型的loader,但它們都會出現一句this._onDataArrival(chunk:ArrayBuffer, byteStart, this._receivedLength);.顯然數據就在chunk里面
7.在flv-dumuxer.js中

    bindDataSource(loader) {
        loader.onDataArrival = this.parseChunks.bind(this);
        return this;
    }

可以看到這個回調,把數據解析交給了parseChunks方法,parseChunks把所有數據解析好之后,會執行this._onDataAvailable(this._audioTrack, this._videoTrack);.這又是一個回調方法,它的調用者就是_remuxer,證據就是

this._remuxer.bindDataSource(this._demuxer
             .bindDataSource(this._ioctl
));

順便看一下這兩個的數據類型

this._videoTrack = {type: 'video', id: 1, sequenceNumber: 0, samples: [], length: 0};
this._audioTrack = {type: 'audio', id: 2, sequenceNumber: 0, samples: [], length: 0};

8.去mp4-remuxer.js
this._onDataAvailable(this._audioTrack, this._videoTrack);上個流程中的這句代碼會跑到remux(audioTrack, videoTrack) {}這個方法里。然后跑到_onTrackMetadataReceived方法,然后調用了this._onInitSegment,這又是一個回調……
9.去transmuxer-controller.js中可以找到如下代碼

    this._remuxer.onInitSegment =this._onRemuxerInitSegmentArrival.bind(this);
    this._remuxer.onMediaSegment = this._onRemuxerMediaSegmentArrival.bind(this);

    _onRemuxerInitSegmentArrival(type, initSegment) {
        this._emitter.emit(TransmuxingEvents.INIT_SEGMENT, type, initSegment);
    }

10.終于TransmuxingEvents.INIT_SEGMENT事件拋出來了。在之前的load方法中,曾經注冊過偵聽

load() {
this._transmuxer = new Transmuxer(this._mediaDataSource, this._config);
this._transmuxer.on(TransmuxingEvents.INIT_SEGMENT, (type, is) => {
    this._msectl.appendInitSegment(is);
});
this._transmuxer.on(TransmuxingEvents.MEDIA_SEGMENT, (type, ms) => {
    this._msectl.appendMediaSegment(ms);
this._transmuxer.on...
this._transmuxer.open();
}

11.進入mse-controller.js

appendInitSegment(initSegment, deferred) {
  ...
  let sb = this._sourceBuffers[is.type] = this._mediaSource.addSourceBuffer(mimeType);
  ...
}

_doAppendSegments() {
  ...
  this._sourceBuffers[type].appendBuffer(segment.data);
  ...
  this._doAppendSegments();
}

appendMediaSegment(mediaSegment) {
  ...
  this._doAppendSegments();
}

可以看到在往_mediaSource里面塞數據,而_mediaSource早就通過attachMediaElement方法綁定了HTML中的video標簽。

    attachMediaElement(mediaElement) {
        if (this._mediaSource) {
            throw new IllegalStateException('MediaSource has been attached to an HTMLMediaElement!');
        }
        let ms = this._mediaSource = new window.MediaSource();

end

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,606評論 6 533
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,582評論 3 418
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,540評論 0 376
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,028評論 1 314
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,801評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,223評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,294評論 3 442
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,442評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,976評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,800評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,996評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,543評論 5 360
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,233評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,662評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,926評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,702評論 3 392
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,991評論 2 374

推薦閱讀更多精彩內容