webo-video
Version:
## 安装 ```javascript npm install webo-video ``` 该库依赖 video.js 以及 flv.js 安装 `webo-video` 之后可直接 `import videojs from "video.js"` 以及 `import flv.js from "flv.js"`
718 lines (643 loc) • 22.9 kB
JavaScript
/* eslint-disable no-cond-assign */
// @ts-nocheck
import videojs from 'video.js/dist/video.js' //
// flv 插件
import flvjs from 'flv.js';
// videojs + flv.js 插件
const Html5 = videojs.getTech('Html5');
const mergeOptions = videojs.mergeOptions || videojs.util.mergeOptions;
const defaults = {
mediaDataSource: {},
config: {}
};
class Flvjs extends Html5 {
/**
* Create an instance of this Tech.
*
* @param {Object} [options]
* The key/value store of player options.
*
* @param {Component~ReadyCallback} ready
* Callback function to call when the `Flvjs` Tech is ready.
*/
constructor(options, ready) {
options = mergeOptions(defaults, options);
super(options, ready);
}
/**
* A getter/setter for the `Flvjs` Tech's source object.
*
* @param {Tech~SourceObject} [src]
* The source object you want to set on the `Flvjs` techs.
*
* @return {Tech~SourceObject|undefined}
* - The current source object when a source is not passed in.
* - undefined when setting
*/
setSrc(src) {
if (this.flvPlayer) {
// Is this necessary to change source?
this.flvPlayer.detachMediaElement();
this.flvPlayer.destroy();
}
const mediaDataSource = this.options_.mediaDataSource;
const config = this.options_.config;
mediaDataSource.type = mediaDataSource.type === undefined ? 'flv' : mediaDataSource.type;
mediaDataSource.url = src;
this.flvPlayer = flvjs.createPlayer(mediaDataSource, config);
this.flvPlayer.attachMediaElement(this.el_);
this.flvPlayer.load();
}
/**
* Dispose of flvjs.
*/
dispose() {
if (this.flvPlayer) {
this.flvPlayer.detachMediaElement();
this.flvPlayer.destroy();
}
super.dispose();
}
}
/**
* Check if the Flvjs tech is currently supported.
*
* @return {boolean}
* - True if the Flvjs tech is supported.
* - False otherwise.
*/
Flvjs.isSupported = function() {
return flvjs && flvjs.isSupported();
};
/**
* Flvjs supported mime types.
*
* @constant {Object}
*/
Flvjs.formats = {
'video/flv': 'FLV',
'video/x-flv': 'FLV'
};
/**
* Check if the tech can support the given type
*
* @param {string} type
* The mimetype to check
* @return {string} 'probably', 'maybe', or '' (empty string)
*/
Flvjs.canPlayType = function(type) {
if (Flvjs.isSupported() && type in Flvjs.formats) {
return 'maybe';
}
return '';
};
/**
* Check if the tech can support the given source
* @param {Object} srcObj
* The source object
* @param {Object} options
* The options passed to the tech
* @return {string} 'probably', 'maybe', or '' (empty string)
*/
Flvjs.canPlaySource = function(srcObj, options) {
return Flvjs.canPlayType(srcObj.type);
};
// Include the version number.
Flvjs.VERSION = '__VERSION__';
videojs.registerTech('Flvjs', Flvjs);
// rtc 类
export class RTC {
url;
defaultPath;
pc
onaddstream;
constructor(url) {
this.url = url;
this.defaultPath = '/rtc/v1/play/';
// 初始化配置
this.pc = new RTCPeerConnection();
// 监听流
this.pc.onaddstream = (event) => {
if (this.onaddstream) {
this.onaddstream(event);
}
};
// // 信号监听更改
// this.pc.onsignalingstatechange = this.onsignalingstatechange;
// // iceconnection 状态更改
// this.pc.oniceconnectionstatechange = this.oniceconnectionstatechange;
// // icegathering 状态更改
// this.pc.onicegatheringstatechange = this.onicegatheringstatechange;
// // 监听轨道
// this.pc.ontrack = this.handleOnTrack;
// // 啥东西
// this.pc.onicecandidate = this.onicecandidate;
}
onsignalingstatechange = function(state) {
console.info('ddddd', state)
};
oniceconnectionstatechange = function(state) {
console.info('ccccc:', state)
};
onicegatheringstatechange = function(state) {
console.info('bbbbb:', state)
};
// 切换轨道
handleOnTrack = function(e) {
console.log('handleOnTrack', e.streams);
// if (self.video.srcObject !== e.streams[0]) {
// console.log('setting video stream from ontrack');
// self.video.srcObject = e.streams[0];
// }
};
onicecandidate = function(e) {
if (e.candidate) {
// self.onWebRtcCandidate(JSON.stringify(e.candidate));
}
};
async play(url) {
// 发起订阅
let conf = this.prepareUrl(url);
this.pc.addTransceiver("audio", { direction: "recvonly" });
this.pc.addTransceiver("video", { direction: "recvonly" });
let offer = await this.pc.createOffer({
offerToReceiveAudio: 1,
offerToReceiveVideo: 1
});
await this.pc.setLocalDescription(offer)
let session = await new Promise((resolve, reject) => {
let data = {
api: conf.apiUrl,
streamurl: conf.streamUrl,
clientip: null,
sdp: offer.sdp
}
fetch(conf.apiUrl.replace("1986", "1985"), {
method: "POST",
body: JSON.stringify(data)
})
.then(response => response.json())
.then(res => {
if (res.code) {
reject(data);
} else {
resolve(res)
}
}).catch(error => {
console.log(error);
})
});
// 接收订阅
await this.pc.setRemoteDescription(
new RTCSessionDescription({ type: 'answer', sdp: session.sdp })
)
// console.log('iceConnectionState', this.pc.iceConnectionState)
// this.pc.onsignalingstatechange = (e) => {
// console.log('onsignalingstatechange', e)
// }
// this.pc.onloadedmetadata = (e) => {
// console.log('onloadedmetadata', e)
// }
// this.pc.iceconnectionstatechange = (e) => {
// console.log('iceconnectionstatechange', e);
// console.log('iceConnectionState1', this.pc.iceConnectionState)
// }
// this.pc.oniceconnectionstatechange = (e) => {
// console.log('oniceconnectionstatechange', e);
// }
return session
}
close() {
this.pc.close();
this.pc = null;
}
prepareUrl(webrtcUrl) {
let urlObject = this.parse(webrtcUrl)
let schema = urlObject.user_query.schema;
schema = schema ? schema + ':' : window.location.protocol;
let port = urlObject.port || 1985;
if (schema === 'https:') {
port = urlObject.port || 443;
}
let api = urlObject.user_query.play || this.defaultPath;
if (api.lastIndexOf('/') !== api.length - 1) {
api += '/';
}
let apiUrl = schema + '//' + urlObject.server + ':' + port + api;
for (let key in urlObject.user_query) {
if (key !== 'api' && key !== 'play') {
apiUrl += '&' + key + '=' + urlObject.user_query[key];
}
}
apiUrl = apiUrl.replace(api + '&', api + '?');
let streamUrl = urlObject.url;
return { apiUrl: apiUrl, streamUrl: streamUrl, schema: schema, urlObject: urlObject, port: port };
}
parse(url) {
let a = document.createElement('a')
a.href = url.replace("rtmp://", "http://")
.replace("webrtc://", "http://")
.replace("rtc://", "http://");
let vhost = a.hostname
let app = a.pathname.substr(1, a.pathname.lastIndexOf("/") - 1)
let stream = a.pathname.substr(a.pathname.lastIndexOf("/") + 1)
app = app.replace("...vhost...", "?vhost=")
if (app.indexOf("?") >= 0) {
let params = app.substr(app.indexOf("?"))
app.substr(0, app.indexOf("?"))
if (params.indexOf("vhost=") > 0) {
vhost = params.substr(params.indexOf("vhost=" + "vhost=".length))
if (vhost.indexOf("&") > 0) {
vhost = vhost.substr(0, vhost.indexOf("&"))
}
}
}
if (a.hostname === vhost) {
let re = /^(\d+)\.(\d+)\.(\d+)\.(\d+)$/
if (re.test(a.hostname)) {
vhost = "__defaultVhost__";
}
}
let schema = "rtmp"
if (url.indexOf("://")) {
schema = url.substr(0, url.indexOf("://"))
}
let port = a.port
if (!port) {
if (schema === 'http') {
port = 80;
} else if (schema === 'https') {
port = 443;
} else if (schema === 'rtmp') {
port = 1935;
}
}
let ret = {
url: url,
schema: schema,
server: a.hostname,
port: port,
vhost: vhost,
app: app,
stream: stream
};
this.fill_query(a.search, ret)
if (!ret.port) {
if (schema === 'webrtc' || schema === 'rtc') {
if (ret.user_query.schema === 'https') {
ret.port = 443;
} else if (window.location.href.indexOf('https://') === 0) {
ret.port = 443;
} else {
// For WebRTC, SRS use 1985 as default API port.
ret.port = 1985;
}
}
}
return ret
}
fill_query(query_string, obj) {
obj.user_query = {}
if (query_string.length === 0) {
return;
}
if (query_string.indexOf("?") >= 0) {
query_string = query_string.split("?")[1];
}
let queries = query_string.split("&");
for (let i = 0; i < queries.length; i++) {
let elem = queries[i];
let query = elem.split("=");
obj[query[0]] = query[1];
obj.user_query[query[0]] = query[1];
}
if (obj.domain) {
obj.vhost = obj.domain;
}
}
}
// 1 加入观察者模式
// 2 添加回调类型 play read error end dbclick click
// 3 发布npm包 webo-video.js
// 基于videojs 播放 flv | webrtc | rtmp | mp4 | hls 等视频格式
export class weboVideojs {
// 播放状态 null ready playing played error
playStatus = null;
// 断流配置
cutoutConfig = {
max: 10, // 最大重复次数
num: 0, // 当前时间重复次数
time: 0 // 当前时间
}
// flv 倍数播放配置
flvConfig = {
maxDelta: 1, // 最大跳帧延迟
rate1: 1.5, // 倍数播放1
maxRateDelta: 1, //最大倍数播放延迟
rate2: 1.5, // 倍数播放2
}
// video 配置
videoConf = {
url: '',
type: '', // flv | webrtc | rtmp | mp4
el: '', // 可以传video dom 或者视频容器盒子
isLive: false, //是否直播
}
// 初始化订阅者
eventObj = {};
// 播放器
player = null;
// 容器
el = null;
// web rtc 容器
RTC = null;
// 定时器
interTime = 0;
constructor(myConfig, videoConfig) {
// video 配置
this.videoConf = this.initVideoConfig(myConfig);
// 初始化播放器
this.initPlay(videoConfig);
}
// 初始化播放器
initPlay(videoConfig) {
// videojs 配置
this.player = videojs(this.el, this.initConfig(videoConfig), () => {
this.player.on('ready', (e) => {
this.emit('ready');
this.setPlayStatus('ready');
});
this.player.on('webkitbeginfullscreen', () => {});
this.player.on('loadedmetadata', () => {});
this.player.on('click', e => {
// this.el.play();
this.emit('click')
});
this.player.on('load', (e) => {
this.emit('load')
})
this.player.on('play', () => {
this.emit('play');
this.clearTime();
this.setPlayStatus('playing');
// 开启定时器
this.createInterTime();
});
this.player.on('error', (e) => {
this.emit('error', e);
this.setPlayStatus('error')
});
this.player.on('ended', () => {
this.emit('ended');
this.setPlayStatus('played')
});
this.player.on('data', (e) => {});
});
}
// this.el 事件监听
eventFun(type) {
let self = this;
function onClcik() {
self.emit('click')
}
let funObj = {
add: () => {
this.el.addEventListener('click', onClcik);
},
remove: () => {
this.el.removeEventListener('click', onClcik);
}
}
funObj[type]();
}
// 事件监听
on(eventName, cb) {
if (!this.eventObj[eventName]) this.eventObj[eventName] = [];
this.eventObj[eventName].push(cb);
}
// 取消监听
off(eventName) {
this.eventObj[eventName] = []
}
// 发布
emit(eventName, ...rest) {
if (!this.eventObj[eventName]) return;
this.eventObj[eventName].forEach(cb => {
cb(rest)
});
}
// 默认配置config
initConfig(config) {
// 默认配置
let params = {
autoplay: true,
controls: false,
}
if (this.videoConf.type == 'flv') {
// flv 默认配置
params = {
autoplay: true, //自动播放
controls: true, //用户可以与之交互的控件
loop: true, //视频一结束就重新开始
muted: true, //默认情况下将使所有音频静音
// aspectRatio: "16:9",//显示比率
fullscreen: {
options: {
navigationUI: 'auto'
}
},
techOrder: ["html5", "flvjs"], // 兼容顺序
flvjs: {
mediaDataSource: {
isLive: false,
cors: true,
withCredentials: false,
},
config: {
autoCleanupMaxBackwardDuration: 20,
autoCleanupMinBackwardDuration: 20,
lazyLoadMaxDuration: 10,
lazyLoadRecoverDuration: 10,
// enableWorker: true, // 启用分离的线程进行转换
enableStashBuffer: false, // 关闭IO隐藏缓冲区
stashInitialSize: 128, // 减少首帧显示等待时长
reuseRedirectedURL: true, //重用301/302重定向url,用于随后的请求,如查找、重新连接等。
autoCleanupSourceBuffer: true //自动清除缓存
},
// sources: [{ src: params.url, type: "video/x-flv" }]
}
}
}
return Object.assign({}, params, config || {})
}
// 默认配置myConfig
initVideoConfig(config) {
if (!config.el) return console.error('视频容器必传');
let isLive = config.type == 'mp4' ? false : true
if (config.el.nodeName == 'VIDEO') {
this.el = config.el;
} else {
this.el = document.createElement('video');
this.el.setAttribute('class', 'video-js');
this.el.setAttribute('autoplay', 'autoplay');
config.el.append(this.el);
}
// 初始化监听双击事件和单机事件
// if (this.el) {
// this.eventFun('add');
// }
return Object.assign({}, {
url: '',
type: '', // flv | webrtc | rtmp | mp4
el: '', // 可以传video dom 或者视频容器盒子
isLive, //是否直播
}, config);
}
// 获取播放器
getPlayer() {
return this.player;
}
// 获取播放容器
getVideoEL() {
return this.el;
}
// 断开连接
dispose() {
// 销毁事件
// if (this.el) {
// this.eventFun('remove');
// }
// this.player.dispose();
this.player = null;
this.el = null;
this.closeRTC();
this.clearTime();
}
clearPlayer() {
this.player.dispose();
this.dispose();
}
// 关闭RTC
closeRTC() {
if (this.RTC) {
this.RTC.close();
this.RTC = null;
}
}
// 关闭定时器
clearTime() {
if (this.interTime) {
clearInterval(this.interTime)
}
}
// 当视频开始播放 创建一个定时器
createInterTime() {
this.interTime = setInterval(() => {
// 是否直播
if (this.videoConf.isLive && this.player && this.player.buffered) {
let end = this.player.buffered().end(0); //获取当前buffered值(缓冲区末尾)
let delta = end - this.player.currentTime(); //获取buffered与当前播放位置的差值
// console.log('流的最后一帧', end, '当前播放帧', this.player.currentTime(), '延迟', delta, '重复次数', this.cutoutConfig.num);
//当视频开始播放 计算播放时间
if (this.player.currentTime() == this.cutoutConfig.time) {
if (this.cutoutConfig.max == this.cutoutConfig.num) {
this.cutoutConfig.num = 0;
this.clearTime();
this.closeRTC();
this.emit('staermError');
this.setPlayStatus('error')
}
this.cutoutConfig.num += 1;
} else {
this.cutoutConfig.time = this.player.currentTime();
this.cutoutConfig.num = 0
}
}
// flv 跳帧
if (this.videoConf.type == 'flv' && this.player && this.player.buffered) {
let end = this.player.buffered().end(0); //获取当前buffered值(缓冲区末尾)
let delta = end - this.player.currentTime(); //获取buffered与当前播放位置的差值
// console.log('流的最后一帧', end, '当前播放帧', this.player.currentTime(), '延迟', delta, '重复次数', this.cutoutConfig.num);
// 延迟过大,通过跳帧的方式更新视频
if (delta > this.flvConfig.maxDelta) {
// 跳帧
// this.player.currentTime = this.player.buffered().end(0) - 0.5;
// 倍数播放
this.player.playbackRate(this.flvConfig.rate1); // 1.5
} else {
// 恢复正常倍速
this.player.playbackRate(1);
}
}
// 当视频正在播放
if (this.getPlayStatus() == 'playing') {
this.emit('playing')
}
if (this.getPlayStatus() == 'read') {
this.cutoutConfig.num += 1;
if (this.cutoutConfig.num == this.cutoutConfig.max) {
this.emit('error', { msg: '视频播放失败' })
}
}
}, 1000);
}
// 设置视频配置
setVideoConfig(obj = { url: '', type: '' }) {
this.videoConf = this.initVideoConfig(obj);
}
// 播放视频
play() {
switch (this.videoConf.type) {
case "mp4":
// mp4 直接将地址给this.demo
if (!this.videoConf.url.endsWith('.mp4')) return console.error('视频地址格式不正确');
this.el.src = this.videoConf.url;
break;
case "webrtc":
if (!this.videoConf.url.startsWith('webrtc')) return console.error('视频地址格式不正确');
this.RTC = new RTC(this.videoConf.url);
this.RTC.onaddstream = (e) => {
this.el.srcObject = e.stream;
// this.el.click();
this.emit('staermLoad')
}
this.RTC.play(this.videoConf.url).then(res => {})
break;
case "hls":
break;
case "flv":
if (!this.videoConf.url.endsWith('.flv')) return console.error('视频地址格式不正确');
// 设置视频流
this.player.src([{ src: this.videoConf.url, type: "video/x-flv" }]);
this.player.load(this.videoConf.url);
this.player.play();
// setTimeout(() => {
// this.el.click();
// }, 1000);
break;
}
}
/**
* 设置播放状态
* @param {*} type
*/
setPlayStatus(type) {
this.playStatus = type;
}
// 返回播放状态
getPlayStatus() {
return this.playStatus;
}
// 设置buffer数据
setBuffer(data) {
this.player.appendBuffer(data)
}
// 设置静音
setMuted(val) {
this.player.muted(Boolean(val));
}
}
const weboVideo = (myConfig, videoConfig) => {
return new weboVideojs(myConfig, videoConfig)
}
export default weboVideo;