前端中的直播
- 2019 年 11 月 28 日
- 筆記
因為公司是做在線抓娃娃的,涉及到直播推流這一部分的工作。之前一直都是在App上面進行遊戲,所以關於直播這一部分也是與Android與IOS有關,與前端是沒有關係的。但是現在新的需求就是要求這個在線抓娃娃要能夠在網頁上面進行遊戲。所以,我的事情來了。對於沒有涉及到前端音影片的這部分的需求,所以初入這一行,還是有點馬馬虎虎,花了一周多的時間終於是弄明白了。
要了解前端影片方面的東西,還是要從基礎的說起。
介紹
2019年了,HTML5已經走進千家萬戶,同時,直播也在全球盛行。App端的姑且不說,web端的使用影片播放的話,一般都是在用HTML5中的 video 標籤了。然而,video標籤的限制實在是太多了,尤其是對於播放格式這一項,僅僅是支援 MP4
OGG
WebM
格式,現在可能還支援 m3u8
格式的影片。
但是,再來看看現在的直播方面的知識
直播簡介
關於直播,大概的過程是:推流—>源站—>客戶端拉流—>客戶端播放
- 推流:指的是把採集階段封包好的內容傳輸到伺服器的過程。
- 拉流:一般是一個URL地址,即播放地址,有多種類型的流。
影片直播服務目前常用的包含三種協議(當前時間阿里雲的直播推流也是這三種協議),分別是RTML, HLS, (HTTP-)FLV。
下面附上一張知乎上面的推流拉流圖

### RTMP: RTMP是Real Time Messaging Protocol(實時消息傳輸協議)的縮寫,是Adobe公司為Flash/AIR平台和伺服器之間音、影片及數據傳輸開發的實時消息傳送協議。RTMP協議基於TCP,包括RTMP基本協議及RTMPT/RTMPS/RTMPE等多種變種。RTMP協議中,影片必須是H264編碼,音頻必須是AAC或MP3編碼,且多以flv格式封包。RTMP是目前最主流的流媒體傳輸協議,對CDN支援良好,實現難度較低,是大多數的直播平台的選擇。不過RTMP有著一個最大的不足——不支援瀏覽器,且Adobe已不再更新。因此直播服務要支援瀏覽器的話,需要另外的推送協議支援。
優點:
- 延時低,穩定性好,支援攝影機格式多
缺點:
- 瀏覽器需要載入flash才可以播放(預計2020年底所有瀏覽器最新版本都不在支援flash)
- RTMP是私有協議(Adobe的私有協議),很多設備無法播放。同時移動端不支援flash。所以,這種格式的影片基本無法再移動端使用。
- 安全性問題
HLS
HLS(Http Live Streaming) 是一個由蘋果公司提出的基於HTTP的流媒體網路傳輸協議,直接把流媒體切片成一段段,資訊保存到m3u(m3u8)
列表文件中, 可以將不同速率的版本切成相應的片;播放器可以直接使用http協議請求流數據。
優點:
- 可以在不同速率的版本間自由切換,實現無縫播放
- 省去使用其他協議的煩惱
缺點:
- 延時高,不適合做直播
- 因為採用ts切片,所以一個文件可能會被切成成百上千個小文件,對存儲和快取都有一定的挑戰
這個流一般用於蘋果web瀏覽器的直播,因為FLV和RMTP都不支援IOS的移動端(手機與平板)
HTTP-FLV
關於FLV在目前來說還是一個更好的方案,關於FLV方面的介紹可以查看使用flv.js做直播。上面有詳細的介紹。
因為bilibili開源flv.js的原因,使得flv在目前的直播中用的更多,尤其是在移動端中使用flv流。
上面的三種方案RTMP是最好的,不管是延時還是性能問題。所以,最好的方案就是PC端採用RTMP,移動端採用HTTP-FLV。但是要考慮一點就是2020年的flash的問題。
前端做直播
在影片播放方面,前端有一個開源的插件videojs。可以播放HTML5的影片格式以及Flash方面的影片。但是,在6.X開始的版本後,videojs不在支援flash,需要單獨引用videojs-flash插件,或者是使用6.X以下的版本。
重點注意:videojs+flash不支援移動端。當初在這裡糾結了兩天的時間,實在是沒有辦法。
我使用的是react框架開發的。
import React from 'react'; import videojs from 'video.js' // import videozhCN from 'video.js/dist/lang/zh-CN.json'; //播放器中文,不能使用.js文件 import 'video.js/dist/video-js.css'; //樣式文件注意要加上 import 'videojs-flash' class VideoPlayer extends React.Component { componentDidMount() { // console.log(flvjs); // instantiate Video.js //這裡的this.props是上級傳進來的video的options console.log(this.props); this.player = videojs(this.videoNode, this.props, function onPlayerReady() { console.log('onPlayerReady', this) }); videojs.addLanguage('zh-CN', videozhCN); // this.player.liveTracker.on('liveedgechange', () => { // console.log('跟隨直播'); // this.player.liveTracker.seekToLiveEdge(); // }); } // destroy player on unmount componentWillUnmount() { if (this.player) { this.player.dispose() } } // wrap the player in a div with a `data-vjs-player` attribute // so videojs won't create additional wrapper in the DOM // see https://github.com/videojs/video.js/pull/3856 render() { return ( <div> <div data-vjs-player> {/*這個帶有屬性的div目前沒看到作用,可以去掉*/} <video ref={ node => this.videoNode = node } className="video-js"></video> </div> </div> ) } } export default VideoPlayer
引用 VideoPlayer
import React, { Component } from 'react' import { storage } from '@utils' import { constant } from '@data/constant' class Game extends Component { constructor(props){ super(props) this.state = { videoSrc: "" } this.videoJsOptions = null } componentDidMount(){ const THIS = this const token = storage.getItem(constant.TOKEN) var toyid = 913 var URL = `wss://XXX.XXX.com//websocket/1.0.0/WEB/${toyid}/${token}` const socket = new WebSocket(URL); socket.onmessage = function(msg){ var data = JSON.parse(msg.data) // var cmd = data.cmd // var code = data.code console.log(data) // 沒有登錄 if (typeof data.data === "string") return // users data.data.room.users.users let users = data.data.room.users.users let currentPlayer = users.filter(v => v.play === true) let waitingPlayers = users.filter(v => v.play === false) let videoSrc = data.data.room.video.split(',')[0] let newState = {} if (currentPlayer.length) { newState.currentPlayer = currentPlayer[0] } if (waitingPlayers.length) { newState.waitingPlayers = waitingPlayers } if (videoSrc) { // videoSrc = videoSrc.replace("rtmp", "http") + ".flv" console.log(videoSrc); newState.videoSrc = videoSrc THIS.setOptions(videoSrc) } THIS.setState(newState) } } setOptions(videoSrc) { const videoContainer = this.refs["video-container"] // 播放器的配置 const videoJsOptions = { autoplay: true, //自動播放 language: 'zh-CN', // controls: true, //控制條 preload: true, //自動載入 // errorDisplay: true, //錯誤展示 width: videoContainer.clientWidth, //寬 height: videoContainer.clientHeight, //高 // fluid: true, //跟隨外層容器變化大小,跟隨的是外層寬度 // controlBar: false, // 設為false不渲染控制條DOM元素,只設置controls為false雖然不展示,但還是存在 // textTrackDisplay: false, // 不渲染字幕相關DOM userActions: { // hotkeys: true //是否支援熱鍵 }, sources: [ { src: videoSrc, // src: 'http://live2.get.cpxlive.com/live2/front124.m3u8', // src: "http://snowman.mobilecpx.com/video/wifi-socket.mp4", // type: "rmtp/flv" type: "application/x-mpegURL", //類型可加可不加,目前未看到影響 // type: 'video/mp4', } ] }; this.videoJsOptions = videoJsOptions } render(){ const { videoSrc } = this.state return ( <div className="cpx-game flex-content"> <section ref="video-container" className="cpx-game-video"> {videoSrc && <VideoPlayer {...this.videoJsOptions}/>} </section> </div> ) } } export default Game
注意注意:上面的影片能夠播放了,但是。播放的時候中間會出現一個

。沒錯,自己點擊播放。真沒意思。如果要解決這個問題,需要引入 video-js.swf
。這個在 node_modules/videojs-swf
下面有一個 video-js.swf 文件。我是直接把把他引入到了

下面。這樣處理完成後就可以自動播放了。真是操蛋了。
VidePlayer.js修改如下
import React from 'react'; import videojs from 'video.js' // import videozhCN from 'video.js/dist/lang/zh-CN.json'; //播放器中文,不能使用.js文件 import 'video.js/dist/video-js.css'; //樣式文件注意要加上 import 'videojs-flash' //如果要播放RTMP要使用flash 需要先npm i videojs-flash // 引用這個中間就不會出現那個 SB 的Flash播放按鈕 + import swf from './video-js.swf' + videojs.options.flash.swf = swf
上面的PC的,採用的是RTMP的流
如果是flv的流,react可以使用reflv這個插件。
import React from 'react'; import Reflv from 'reflv' class VideoPlayer extends React.Component { render() { const {url} = this.props return ( <Reflv url={url} type="flv" isLive cors /> ) } } export default VideoPlayer
更多的配置參數查看https://gwuhaolin.github.io/reflv/