前端中的直播
- 2019 年 11 月 28 日
- 筆記
因为公司是做在线抓娃娃的,涉及到直播推流这一部分的工作。之前一直都是在App上面进行游戏,所以关于直播这一部分也是与安卓与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/