当前位置: 首页 > news >正文

wfs.js之h264转码mp4分析

准备源文件

下载源文件

git clone https://github.com/ChihChengYang/wfs.js.git

编译后得到wfs.js这个文件

调用

在demo/index.html中,前端对wfs.js进行了调用

var video1 = document.getElementById("video1"),
wfs = new Wfs();    
wfs.attachMedia(video1,'ch1');

对应wfs.js中的function Wfs() 函数

key: 'attachMedia',
    value: function attachMedia(media) {
      //arg1 通道
      var channelName = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 'chX';
      //arg2 媒体格式
      var mediaType = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : 'H264Raw';
      //arg3 未定义
      var websocketName = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : 'play2';
      // 'H264Raw' 'FMp4'    
      this.mediaType = mediaType;
      this.media = media;
      //MEDIA_ATTACHING 事件处理
      this.trigger(_events2.default.MEDIA_ATTACHING, { media: media, channelName: channelName, mediaType: mediaType, websocketName: websocketName });

MEDIA_ATTACHING 事件的处理

MEDIA_ATTACHING: 'wfsMediaAttaching',

在var BufferController = function (_EventHandler)中

    key: 'onMediaAttaching',
    value: function onMediaAttaching(data) {
      var media = this.media = data.media;
      this.mediaType = data.mediaType;
      this.websocketName = data.websocketName;
      this.channelName = data.channelName;
      if (media) {
        // setup the media source
        var ms = this.mediaSource = new MediaSource();
        //Media Source listeners
        //绑定ms的监听事件
        //打开时候onMediaSourceOpen
        this.onmso = this.onMediaSourceOpen.bind(this);
        this.onmse = this.onMediaSourceEnded.bind(this);
        this.onmsc = this.onMediaSourceClose.bind(this);
        ms.addEventListener('sourceopen', this.onmso);
        ms.addEventListener('sourceended', this.onmse);
        ms.addEventListener('sourceclose', this.onmsc);
        // link video and media Source
        //连接video和刚刚创建的ms media Source
        media.src = URL.createObjectURL(ms);
      }
    }

ms的onMediaSourceOpen

在ms被打开的时候。调用 onMediaSourceOpen

key: 'onMediaSourceOpen',
    value: function onMediaSourceOpen() {
      var mediaSource = this.mediaSource;
      if (mediaSource) {
        // once received, don't listen anymore to sourceopen event
        mediaSource.removeEventListener('sourceopen', this.onmso);
      }

      if (this.mediaType === 'FMp4') {
        this.checkPendingTracks();
      }

      //调用MEDIA_ATTACHED事件处理
      this.wfs.trigger(_events2.default.MEDIA_ATTACHED, { media: this.media, channelName: this.channelName, mediaType: this.mediaType, websocketName: this.websocketName });

MEDIA_ATTACHED事件处理

MEDIA_ATTACHED: 'wfsMediaAttached',
    key: 'onMediaAttached',
    value: function onMediaAttached(data) {
      if (data.websocketName != undefined) {
        //创建WebSocket连接
        var client = new WebSocket('ws://' + window.location.host + '/' + data.websocketName);
        //调用attachWebsocket方法
        this.wfs.attachWebsocket(client, data.channelName);
      } else {
        console.log('websocketName ERROE!!!');
      }
    }
    key: 'attachWebsocket',
    value: function attachWebsocket(websocket, channelName) {
      //调用WEBSOCKET_ATTACHING事件处理
      this.trigger(_events2.default.WEBSOCKET_ATTACHING, { websocket: websocket, mediaType: this.mediaType, channelName: channelName });
    }

WEBSOCKET_ATTACHING事件处理

WEBSOCKET_ATTACHING: 'wfsWebsocketAttaching',
    key: 'onWebsocketAttaching',
    value: function onWebsocketAttaching(data) {
      this.mediaType = data.mediaType;
      this.channelName = data.channelName;
      if (data.websocket instanceof WebSocket) {
        this.client = data.websocket;
        //websocket 的 onopen事件绑定绑定initSocketClient
        this.client.onopen = this.initSocketClient.bind(this);
        this.client.onclose = function (e) {
          console.log('Websocket Disconnected!');
        };
      }
    }
    key: 'initSocketClient',
    value: function initSocketClient(client) {
      this.client.binaryType = 'arraybuffer';
      //websocket 的 onmessage 事件绑定绑定receiveSocketMessage
      this.client.onmessage = this.receiveSocketMessage.bind(this);
      //WEBSOCKET_MESSAGE_SENDING事件处理
      this.wfs.trigger(_events2.default.WEBSOCKET_MESSAGE_SENDING, { commandType: "open", channelName: this.channelName, commandValue: "NA" });
      console.log('Websocket Open!');
    }

//WEBSOCKET_MESSAGE_SENDING事件处理

WEBSOCKET_MESSAGE_SENDING: 'wfsWebsocketMessageSending'
    key: 'onWebsocketMessageSending',
    value: function onWebsocketMessageSending(event) {
      this.client.send(JSON.stringify({ t: event.commandType, c: event.channelName, v: event.commandValue }));
    }

这样把信息通过json发送出去。

接收信息的处理

上面websocket 的 onmessage 事件绑定绑定receiveSocketMessage, 当websocket接收到信息时,调用receiveSocketMessage

    key: 'receiveSocketMessage',
    value: function receiveSocketMessage(event) {
      if (document['hidden']) return;
      this.buf = new Uint8Array(event.data);
      var copy = new Uint8Array(this.buf);

      if (this.mediaType === 'FMp4') {
        this.wfs.trigger(_events2.default.WEBSOCKET_ATTACHED, { payload: copy });
      }
      if (this.mediaType === 'H264Raw') {
        //H264_DATA_PARSING事件处理
        this.wfs.trigger(_events2.default.H264_DATA_PARSING, { data: copy });
      }
    }

H264_DATA_PARSING事件处理

H264_DATA_PARSING: 'wfsH264DataParsing',
        key: 'onH264DataParsing',
        value: function onH264DataParsing(event) {
            //读取数据
            this._read(event.data);
            var $this = this;
            //对于每个nal
            this.nals.forEach(function (nal) {
                //H264_DATA_PARSED事件处理
                $this.wfs.trigger(_events2.default.H264_DATA_PARSED, {
                    data: nal
                });
            });
        }

H264_DATA_PARSED事件处理

H264_DATA_PARSED: 'wfsH264DataParsed',
    key: 'onH264DataParsed',
    value: function onH264DataParsed(event) {
      //分析AVCTrack
      this._parseAVCTrack(event.data);
      if (this.browserType === 1 || this._avcTrack.samples.length >= 20) {
        // Firefox
        this.remuxer.pushVideo(0, this.sn, this._avcTrack, this.timeOffset, this.contiguous);
        this.sn += 1;
      }
    }

数据经过_parseAVCTrack处理后,调用remuxer.pushVideo方法

    key: 'pushVideo',
    value: function pushVideo(level, sn, videoTrack, timeOffset, contiguous) {
      this.level = level;
      this.sn = sn;
      var videoData = void 0;
      // generate Init Segment if needed
      if (!this.ISGenerated) {
        this.generateVideoIS(videoTrack, timeOffset);
      }
      if (this.ISGenerated) {
        if (videoTrack.samples.length) {
          this.remuxVideo_2(videoTrack, timeOffset, contiguous);
        }
      }
    }

最后调用remuxVideo_2方法

    key: 'remuxVideo_2',
    value: function remuxVideo_2(track, timeOffset, contiguous, audioTrackLength) {
      var offset = 8,
          pesTimeScale = this.PES_TIMESCALE,
          pes2mp4ScaleFactor = this.PES2MP4SCALEFACTOR,
          mp4SampleDuration,
          mdat,
          moof,
          firstPTS,
          firstDTS,
          nextDTS,
          inputSamples = track.samples,
          outputSamples = [];

      /* concatenate the video data and construct the mdat in place
        (need 8 more bytes to fill length and mpdat type) */
      mdat = new Uint8Array(track.len + 4 * track.nbNalu + 8);
      var view = new DataView(mdat.buffer);
      view.setUint32(0, mdat.byteLength);
      mdat.set(_mp4Generator2.default.types.mdat, 4);
      var sampleDuration = 0;
      var ptsnorm = void 0,
          dtsnorm = void 0,
          mp4Sample = void 0,
          lastDTS = void 0;

      for (var i = 0; i < inputSamples.length; i++) {
        var avcSample = inputSamples[i],
            mp4SampleLength = 0,
            compositionTimeOffset = void 0;
        // convert NALU bitstream to MP4 format (prepend NALU with size field)
        while (avcSample.units.units.length) {
          var unit = avcSample.units.units.shift();
          view.setUint32(offset, unit.data.byteLength);
          offset += 4;
          mdat.set(unit.data, offset);
          offset += unit.data.byteLength;
          mp4SampleLength += 4 + unit.data.byteLength;
        }

        var pts = avcSample.pts - this._initPTS;
        var dts = avcSample.dts - this._initDTS;
        dts = Math.min(pts, dts);

        if (lastDTS !== undefined) {
          ptsnorm = this._PTSNormalize(pts, lastDTS);
          dtsnorm = this._PTSNormalize(dts, lastDTS);
          sampleDuration = dtsnorm - lastDTS;
          if (sampleDuration <= 0) {
            _logger.logger.log('invalid sample duration at PTS/DTS: ' + avcSample.pts + '/' + avcSample.dts + '|dts norm: ' + dtsnorm + '|lastDTS: ' + lastDTS + ':' + sampleDuration);
            sampleDuration = 1;
          }
        } else {
          var nextAvcDts = this.nextAvcDts,
              delta;
          ptsnorm = this._PTSNormalize(pts, nextAvcDts);
          dtsnorm = this._PTSNormalize(dts, nextAvcDts);
          if (nextAvcDts) {
            delta = Math.round(dtsnorm - nextAvcDts);
            if ( /*contiguous ||*/Math.abs(delta) < 600) {
              if (delta) {
                if (delta > 1) {
                  _logger.logger.log('AVC:' + delta + ' ms hole between fragments detected,filling it');
                } else if (delta < -1) {
                  _logger.logger.log('AVC:' + -delta + ' ms overlapping between fragments detected');
                }
                dtsnorm = nextAvcDts;
                ptsnorm = Math.max(ptsnorm - delta, dtsnorm);
                _logger.logger.log('Video/PTS/DTS adjusted: ' + ptsnorm + '/' + dtsnorm + ',delta:' + delta);
              }
            }
          }
          this.firstPTS = Math.max(0, ptsnorm);
          this.firstDTS = Math.max(0, dtsnorm);
          sampleDuration = 0.03;
        }

        outputSamples.push({
          size: mp4SampleLength,
          duration: this.H264_TIMEBASE,
          cts: 0,
          flags: {
            isLeading: 0,
            isDependedOn: 0,
            hasRedundancy: 0,
            degradPrio: 0,
            dependsOn: avcSample.key ? 2 : 1,
            isNonSync: avcSample.key ? 0 : 1
          }
        });
        lastDTS = dtsnorm;
      }

      var lastSampleDuration = 0;
      if (outputSamples.length >= 2) {
        lastSampleDuration = outputSamples[outputSamples.length - 2].duration;
        outputSamples[0].duration = lastSampleDuration;
      }
      this.nextAvcDts = dtsnorm + lastSampleDuration;
      var dropped = track.dropped;
      track.len = 0;
      track.nbNalu = 0;
      track.dropped = 0;
      if (outputSamples.length && navigator.userAgent.toLowerCase().indexOf('chrome') > -1) {
        var flags = outputSamples[0].flags;
        flags.dependsOn = 2;
        flags.isNonSync = 0;
      }
      track.samples = outputSamples;
      moof = _mp4Generator2.default.moof(track.sequenceNumber++, dtsnorm, track);
      track.samples = [];

      var data = {
        id: this.id,
        level: this.level,
        sn: this.sn,
        data1: moof,
        data2: mdat,
        startPTS: ptsnorm,
        endPTS: ptsnorm,
        startDTS: dtsnorm,
        endDTS: dtsnorm,
        type: 'video',
        nb: outputSamples.length,
        dropped: dropped
      };

      this.observer.trigger(_events2.default.FRAG_PARSING_DATA, data);
      return data;
    }

在处理完数据后,进行FRAG_PARSING_DATA事件处理

  FRAG_PARSING_DATA: 'wfsFragParsingData',
    key: 'onFragParsingData',
    value: function onFragParsingData(data) {
      var _this2 = this;

      if (data.type === 'video') {}

      [data.data1, data.data2].forEach(function (buffer) {
        if (buffer) {
          _this2.pendingAppending++;
          _this2.wfs.trigger(_events2.default.BUFFER_APPENDING, { type: data.type, data: buffer, parent: 'main' });
        }
      });
    }

BUFFER_APPENDING事件处理

BUFFER_APPENDING: 'wfsBufferAppending',
    key: 'onBufferAppending',
    value: function onBufferAppending(data) {
      if (!this.segments) {
        this.segments = [data];
      } else {
        this.segments.push(data);
      }
      this.doAppending();
    }
    key: 'doAppending',
    value: function doAppending() {

      var wfs = this.wfs,
          sourceBuffer = this.sourceBuffer,
          segments = this.segments;
      if (Object.keys(sourceBuffer).length) {

        if (this.media.error) {
          this.segments = [];
          console.log('trying to append although a media error occured, flush segment and abort');
          return;
        }
        if (this.appending) {
          return;
        }

        if (segments && segments.length) {
          var segment = segments.shift();
          try {
            if (sourceBuffer[segment.type]) {
              this.parent = segment.parent;
              sourceBuffer[segment.type].appendBuffer(segment.data);
              this.appendError = 0;
              this.appended++;
              this.appending = true;
            } else {}
          } catch (err) {
            // in case any error occured while appending, put back segment in segments table 
            segments.unshift(segment);
            var event = { type: ErrorTypes.MEDIA_ERROR };
            if (err.code !== 22) {
              if (this.appendError) {
                this.appendError++;
              } else {
                this.appendError = 1;
              }
              event.details = ErrorDetails.BUFFER_APPEND_ERROR;
              event.frag = this.fragCurrent;
              if (this.appendError > wfs.config.appendErrorMaxRetry) {
                segments = [];
                event.fatal = true;
                return;
              } else {
                event.fatal = false;
              }
            } else {
              this.segments = [];
              event.details = ErrorDetails.BUFFER_FULL_ERROR;
              return;
            }
          }
        }
      }
    }

通过sourceBuffer[segment.type].appendBuffer(segment.data);把数据发送到video去。

相关文章:

  • python 语法篇(一)
  • 从理论到实践:WGS84与GCJ02坐标系详解及腾讯API坐标转换指南,奥维地图坐标转换
  • 非手性分子发光有妙招:借液晶之力,实现高不对称圆偏振发光
  • JavaScript函数详解
  • 向量数据库介绍及应用
  • 影响HTTP网络请求的因素
  • LeetCode算法题(Go语言实现)_20
  • IPv6 网络访问异常 | 时好时坏 / 部分访问正常
  • STM32H743学习记录
  • SpringBoot (二) 日志系统
  • Python+拉普拉斯变换求解微分方程
  • 如何使用stable diffusion 3获得最佳效果
  • Zynq + FreeRTOS 笔试题1
  • STC89C52单片机学习——第37节: [17-1] 红外遥控(外部中断)
  • 详解list容器
  • socket演示程序2
  • xshell可以ssh连接,但vscode不行
  • 多路IO复用-----epoll和poll和select的区别
  • Windows系统本地部署DeepSeek详细教程
  • Ubuntu修改用户名
  • cpa网站怎么做/武汉百度推广优化
  • 怎么做刷网站流量生意/全网营销系统是不是传销
  • web前端做网站地图/关键词网站排名查询
  • 台山政府网站集约化建设/北京、广州最新发布
  • 山西建设机械网站/站长工具忘忧草
  • 电脑单页网站建设/推广方式有哪几种