第六屆360前端星計劃_Web前端點播直播入門

主講人:胡尊傑

一、什麼是視頻

瞭解媒體數據存儲和應用的基礎原理。

  1. 格式與內容
  • 文件擴展名≈媒體封裝格式(媒體容器類型)
    媒體封裝格式≠音視頻編碼格式(使用了誰家的編碼器)
  • 文件內容:
    1)頭信息(格式、時長、幀率、碼率、分辨率…)
    2)索引信息
    3)視頻數據
    4)音頻數據
    5)附加增強數據…
  1. 視頻數據
  • 顯示器顏色呈現基於RGB(紅綠藍)顏色空間模型
  • 視頻領域大多基於YUV顏色空間做抽樣存儲
  • 幀內預測&幀間預測複用進一步有效的壓縮數據
  • P幀(前向預測幀)、B幀(雙向預測幀)、I幀(參考幀)
  • 基於通用標準集N多技術於一身 — 視頻編碼器
    H.264(AVC)、H.265(HEVC)、VP8、VP9…
  1. 音頻數據
  • 聲音:不同振幅&頻率而產生的機械波;數字形式是一維波形
  • 對自然中連續的聲波採樣,做數字化PCM存儲
  • 揚聲器還原PCM(脈衝編碼調製)數字信號爲模擬音頻信號
  • 音頻壓縮基本算法:預測、變換
  • 基於通用標準集N多技術於一身 — 音頻編碼器​​​​​​​
    AAC、MP3…
  1. 傳輸協議
  • 傳統場景
    1)流媒體(直播)
    HLS:蘋果爲利用現有CDN設施而發明的"流媒體"協議
    HTTP(S)-FLV:基於HTTP的流媒體協議
    RTMP、RTP/RTSP、TS、MMS…
    2)點播傳輸
    HTTP(S):通過Range方式或參數方式完成Seek

  • Web端
    HTTP(S)、WS(S)、P2P…

  1. 播放器原理
  • 解協議(加載數據)
  • 解封裝(解複用)
  • 解碼
  • 渲染
  1. 小結
  • 本節介紹了:視頻格式&內容容器、視音頻編碼、傳輸協議、播放器原理。
  • 通過這些信息的瞭解,能大致掌握視頻存儲和應用的基本原理。

二、好玩的Web端API

瞭解通過Web端接口可以實現哪些方向的具體應用。

  1. 媒體兼容判斷
  2. 交互式視頻
  3. 播放本地視頻文件
  4. 播放硬件資源(調用攝像頭或麥克風)
  5. 實現視頻錄製
  6. 播放JS拉取的媒體數據
    判斷瀏覽器端視頻兼容情況
let videoEl = document.createElement("video");
let types = {
  'mp4': 'audio/mp4',
  'MP4': 'video/mp4; codecs="avc1.42E01E, mp4a.40.2"',
  'webm': 'video/webm; codecs="vp8, vorbis"',
  'ogg': 'video/ogg; codecs="theora, vorbis"',
  'm3u8': 'application/vnd.apple.mpegURL',
  'ts': 'video/mp2t; codecs="avc1.42E01E,mp4a.40.2"'
};

Object.keys(types).forEach(key => {
  const type = types[key];
  const ret = videoEl.canPlayType(type) || '不支持';
  console.log(key + ': ' + ret);
});

基於Video時間軸控制實現交互式視頻

let video = $('video');

video.ontimeupdate = ()=>{
  let {currentTime} = video;
  show(currentTime > 64 ? '.s2' : '.s1');
  hide(currentTime > 64 ? '.s1' : '.s2');
  if(
     (currentTime > 64 && currentTime < 65) || 
     (currentTime > 113 && currentTime < 114)
  ){
    video.pause();
  }
};

let ppBtn = $('paly_pause');
video.onplay = ()=>{
  ppBtn.innerText = '暫停';
};
video.onpause = ()=>{
  ppBtn.innerText = '播放';
};
ppBtn.onclick = ()=>{
  video[video.paused ? 'play' : 'pause' ]();
};
$('start').onclick = ()=>{
  video.currentTime = 1;
  video.play();
};
$('step').onclick = ()=>{
  video.currentTime = 60;
  video.play();
};
$('dream').onclick = ()=>{
  video.currentTime = 83;
  video.play();
};
$('drink').onclick = ()=>{
  video.currentTime = 116;
  video.play();
};

hide('.s2');
function show(sel){
  document.querySelectorAll(sel).forEach(el=>{
    el.style.display='inline'
  });
}
function hide(sel){
  document.querySelectorAll(sel).forEach(el=>{
    el.style.display='none'
  });
}
function $(id){
  return document.getElementById(id);
}

基於 FileReader API 播放本地文件

let iptFileEl = document.querySelector('input[type="file"]');
let videoEl = document.querySelector('video');

iptFileEl.onchange = e =>{
  let file = iptFileEl.files && iptFileEl.files[0];
  playFile(file);
};

function playFile(file){
  if(file){
    let fileReader = new FileReader();
    fileReader.onload = evt => {
      if(FileReader.DONE == fileReader.readyState){
        videoEl.src = fileReader.result;
      }else{
        console.log('FileReader Error:', evt);
      }
    }
    fileReader.readAsDataURL(file);
  }else{
    videoEl.src = '';
  }
}

基於 getUserMedia 調用攝像頭或麥克風

const getUserMediaPromise = options => new Promise((resolve, reject) => {
  const nvgt = window.navigator;
  if(nvgt) {
    if(nvgt.mediaDevices && nvgt.mediaDevices.getUserMedia) {
      return nvgt.mediaDevices.getUserMedia(options).then(resolve, reject);
    }
    const getUserMedia = nvgt.getUserMedia || nvgt.webkitGetUserMedia || nvgt.mozGetUserMedia;
    if(getUserMedia) {
      return getUserMedia(options, resolve, reject)
    }
  }
  reject('當前環境不支持獲取媒體設備。');
});

let streamTrack;
const video = document.querySelector('video');
document.querySelector('#play').onclick = () => {
  getUserMediaPromise({
    audio: false,
    video: true
  }).then(stream => {
    video.srcObject = stream;
    streamTrack = stream.getTracks()[0];
  },
  err => {
    console.log('getUserMedia error: [' + err.name + '] ' + err.message)
  });
};




document.querySelector('#stop').onclick = () => {
  streamTrack && streamTrack.stop();
};

const box = document.querySelector('div');
document.querySelector('#sketch').onclick = () => {
  box.className = box.className ==='' ? 'sketch' : '';
};

基於 getUserMedia、MediaRecorder 實現錄像

const getUserMediaPromise = options => new Promise((resolve, reject) => {
  const nvgt = window.navigator;
  if(nvgt) {
    if(nvgt.mediaDevices && nvgt.mediaDevices.getUserMedia) {
      return nvgt.mediaDevices.getUserMedia(options).then(resolve, reject);
    }
    const getUserMedia = nvgt.getUserMedia || nvgt.webkitGetUserMedia || nvgt.mozGetUserMedia;
    if(getUserMedia) {
      return getUserMedia(options, resolve, reject)
    }
  }
  reject('當前環境不支持獲取媒體設備。');
});

const video = document.querySelector('#preview');

let cameraStream;
const opencameraBtn = document.querySelector('#opencamera');
const closecameraBtn = document.querySelector('#closecamera');
const recordBtn = document.querySelector('#record');
const stopRecordBtn = document.querySelector('#stoprecord');
const playBtn = document.querySelector('#play');
const downloadBtn = document.querySelector('#download');

opencameraBtn.onclick = () => getUserMediaPromise({
  audio: false,
  video: true
}).then(
  stream => {
    cameraStream = video.srcObject = stream;
    opencameraBtn.disabled = true;
    closecameraBtn.disabled = false;
    recordBtn.disabled = false;
  },
  err => {
    console.log('getUserMedia error: [' + err.name + '] ' + err.message)
  }
);

closecameraBtn.onclick = () => {
  cameraStream && cameraStream.getTracks()[0].stop();
  cameraStream = null;
  opencameraBtn.disabled = false;
  closecameraBtn.disabled = true;
  stopRecordBtn.onclick();
};

let mediaRecorder;
let recordedBlobs;
const mimeType = ['video/webm;codecs=vp9', 'video/webm;codecs=vp8', 'video/webm', ''].find(type => {
  return MediaRecorder.isTypeSupported(type);
});
// console.log('mimeType', mimeType);
recordBtn.onclick = () => {
  recordedBlobs = [];
  try {
    mediaRecorder = new MediaRecorder(cameraStream, { mimeType });
  } catch(e) {
    alert('Exception while creating MediaRecorder: ' + e + '. mimeType: ' + mimeType);
    return;
  }
  recordBtn.disabled = true;
  stopRecordBtn.disabled = false;
  playBtn.disabled = true;
  downloadBtn.disabled = true;
  mediaRecorder.onstop = evt => {
    console.log('Recorder stopped');
  };
  mediaRecorder.ondataavailable = function(event) {
    if (event.data && event.data.size > 0) {
      recordedBlobs.push(event.data);
    }
  };
  mediaRecorder.start(20); // 單次收集數據毫秒時長,ondataavailable 觸發頻率時長間隔
};


const recordedVideo = document.querySelector('#recorded');
stopRecordBtn.onclick = () => {
  mediaRecorder && mediaRecorder.stop();
  mediaRecorder = null;
  // console.log('Recorded Blobs: ', recordedBlobs);
  recordedVideo.controls = true;
  playBtn.disabled = false;
  downloadBtn.disabled = false;
  stopRecordBtn.disabled = true;
  if(!cameraStream) {
    recordBtn.disabled = true;
  }
};

const getRecordedBlobUrl = () => {
  const superBuffer = new Blob(recordedBlobs, {type: mimeType.split(';')[0]});
  return window.URL.createObjectURL(superBuffer);
};

playBtn.onclick = () => {
  recordedVideo.src = getRecordedBlobUrl();
}

downloadBtn.onclick = () => {
  var a = document.createElement('a');
  a.style.display = 'none';
  a.href = getRecordedBlobUrl();
  a.download = 'test.webm';
  document.body.appendChild(a);
  a.click();
  setTimeout(function() {
    document.body.removeChild(a);
    window.URL.revokeObjectURL(url);
  }, 100);
}


基於MediaSource播放JS拉取的媒體數據

const video = document.querySelector('video');
const fetchMp4 = (url, cb) => { 
  const xhr = new XMLHttpRequest();
  xhr.open('get', url);
  xhr.responseType = 'arraybuffer';
  xhr.onload = function () {
    cb(xhr.response);
  };
  xhr.send();
};

const assetURL = 'https://nickdesaulniers.github.io/netfix/demo/frag_bunny.mp4';
const mimeCodec = 'video/mp4; codecs="avc1.42E01E, mp4a.40.2"';

// 創建動態媒體源,並關聯到video元素上
const mediaSource = new MediaSource(); 
video.src = URL.createObjectURL(mediaSource);

mediaSource.addEventListener('sourceopen', () => {
  const sourceBuffer = mediaSource.addSourceBuffer(mimeCodec);
  // 拉取數據
  fetchMp4(assetURL, buf => {
    sourceBuffer.addEventListener('updateend', () => {
      // 媒體流傳輸完畢
      mediaSource.endOfStream();
      // video.play();
    });
    // 將數據餵給 Video -- 注意這裏只是一次性輸入整個MP4數據
    sourceBuffer.appendBuffer(buf);
  });
});

小結
本節通過具體的實例介紹了:視頻播放控制、數據獲取&採集、錄製存儲、創建動態媒體源播放。

本章節涉及內容配合網絡存儲、實時傳輸,可以試着實現好玩的應用,而且不限於點播或直播。

三、Web端點播直播&播放方案

  1. 點播直播的區別
  • 應用流程
    點播:創作者 => 上傳 => 轉碼 => 存儲 <=> CDN分發 <=> 觀衆
    直播:創作者 => 推流 <=> 存儲 <=> 轉碼 <=> CDN分發 <=> 觀衆
  • 媒體類型的選擇
    HTTP(S)-MP4…:點播服務
    HTTP(S)-FLV:點播、直播
    HTTP(S)-HLS:點播、直播(高延遲)
  1. 播放器解決方案
  • 原生瀏覽器支持的:直接走原生Video播放
  • 原生瀏覽器不支持的
    1)協議或容器類型不支持
    JS解協議下載數據、解容器、重新封裝,然後通過MSE餵給Video解碼、渲染播放:例如Web端播放FLV、HLS:http://chimee.org
    2)解碼器不支持
    JS下載數據,WASM 解容器、解碼,通過 WebGL&WebAudio 渲染播放:例如Web端播放HEVC編碼視頻:https://zyun.360.cn/developer/doc?did=QHWWPlayer
    3)有解密需求的
    參考前兩條,在解容器之後對每幀數據啓用解密邏輯。
  1. 小結
  • 本節通過具體的實例介紹了:點播直播業務流程的不同,Web端播放器解決方案的差異。

  • 目前來說Web端媒體選型,必須基於瀏覽器所支持的能力邊界:如協議類型、容器類型、解碼能力,亦或可自行轉容器(HLS\FLV)、自行解碼(WASM)的能力。

四、參考資料

基礎API:
https://developer.mozilla.org/zh-CN/docs/Web/Guide/HTML/Using_HTML5_audio_and_video

數據獲取:
https://developer.mozilla.org/zh-CN/docs/Web/API/MediaDevices/getUserMedia
https://developer.mozilla.org/zh-CN/docs/Web/API/FileReader
https://developer.mozilla.org/zh-CN/docs/Web/API/XMLHttpRequest
https://developer.mozilla.org/zh-CN/docs/Web/API/Fetch_API

虛擬文件:
https://developer.mozilla.org/zh-CN/docs/Web/API/Blob
https://developer.mozilla.org/zh-CN/docs/Web/API/URL/createObjectURL

動態媒體源:
https://developer.mozilla.org/zh-CN/docs/Web/API/Media_Source_Extensions_API

數據操作:
https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/TypedArray
https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/WebAssembly
https://developer.mozilla.org/zh-CN/docs/Web/API/Web_Workers_API

畫音渲染:
https://developer.mozilla.org/zh-CN/docs/Web/API/WebGL_API
https://developer.mozilla.org/zh-CN/docs/Web/API/Web_Audio_API

場景應用:
https://developer.mozilla.org/zh-CN/docs/Web/API/MediaRecorder
https://developer.mozilla.org/zh-CN/docs/Web/API/WebRTC_API

開源項目:
http://chimee.org
https://github.com/bilibili/flv.js
https://github.com/video-dev/hls.js
https://github.com/huzunjie/WasmVideoPlayer

非開源 WasmVideoPlayer 示例:
http://lab.pyzy.net/qhww

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章