安装flvjs
npm install flvjs --save
封装成一个组件
- 在init方法中引入,因为nextjs是服务端渲染,所以需要这种方式引入
flvjs = (await import('flv.js')).default;
- 需要考虑视频流播放的优化:重连、追帧等
/// FlvPlayer.tsx
'use client';
// import flvjs from 'flv.js'
import Image from 'next/image';
import React, { useEffect, useRef, useState } from 'react';
interface FlvPlayerProps {
className?: string | undefined;
style?: React.CSSProperties;
url: string;
type?: 'flv' | 'mp4';
isLive?: boolean;
controls?: boolean | undefined;
autoPlay?: boolean | undefined;
muted?: boolean | undefined;
height?: number | string | undefined;
width?: number | string | undefined;
videoProps?: React.DetailedHTMLProps<
React.VideoHTMLAttributes<HTMLVideoElement>,
HTMLVideoElement
>;
// flvMediaSourceOptions?: flvjs.MediaDataSource
// flvConfig?: flvjs.Config
flvMediaSourceOptions?: any;
flvConfig?: any;
onError?: (err: any) => void;
}
let flvjs: any;
const maxReloadCount = 100; //最大重连次数
let count = 0;
let lastDecodedFrames = 0;
let stuckTime = 0;
const liveOptimizeConfig = {
//启用 IO 存储缓冲区。如果您需要实时(最小延迟)进行实时流播放,则设置为 false,但如果存在网络抖动,则可能会停止。
enableStashBuffer: false,
//启用分离线程进行传输复用(目前不稳定)
// enableWorker: true,
// 减少首帧显示等待时长
stashInitialSize: 128, //IO暂存缓冲区初始大小
autoCleanupSourceBuffer: true, //对SourceBuffer进行自动清理缓存
autoCleanupMaxBackwardDuration: 60, // 当向后缓冲区持续时间超过此值(以秒为单位)时,请对SourceBuffer进行自动清理
autoCleanupMinBackwardDuration: 40, // 指示进行自动清除时为反向缓冲区保留的持续时间(以秒为单位)。
};
let flvPlayer: any;
const FlvPlayer: React.FC<FlvPlayerProps> = (props) => {
const {
className,
style,
url,
type = 'flv',
isLive = true,
controls,
autoPlay,
// muted = 'muted',
// muted = true,
height,
width,
videoProps,
flvMediaSourceOptions,
flvConfig,
onError,
} = props;
const videoRef = useRef<HTMLVideoElement>(null);
const [muted, setMuted] = useState(true);
useEffect(() => {
if (!url) return;
init();
const handleOnlineStatusChange = () => {
if (navigator.onLine) {
console.log('网络连接状态🎃');
rebuild();
}
};
// 监听网络连接状态变化,网络重连是,重载
window.addEventListener('online', handleOnlineStatusChange);
// 不暂停直播流
if (videoRef?.current) {
videoRef?.current?.addEventListener('pause', () => {
console.log('暂停了,继续播放');
videoRef.current?.play();
});
}
return () => {
if (flvPlayer) {
// 销毁player
flvPlayer?.pause();
flvPlayer?.unload();
flvPlayer?.detachMediaElement();
flvPlayer?.destroy();
flvPlayer = null;
}
if (videoRef.current) {
// 销毁video
videoRef.current?.pause();
videoRef.current?.removeAttribute('src');
//调用 load() 方法,以确保所有相关数据都被卸载。
videoRef.current?.load();
}
videoRef.current?.removeEventListener('pause', () => {});
window.removeEventListener('online', handleOnlineStatusChange);
};
}, [url]);
const init = async () => {
console.log('加载直播流--------------------------------------------------------');
try {
flvjs = (await import('flv.js')).default;
if (flvjs.isSupported() && videoRef.current) {
flvPlayer = flvjs.createPlayer(
{
type,
url,
isLive,
...flvMediaSourceOptions,
},
{
...flvConfig,
...(isLive ? liveOptimizeConfig : {}),
},
);
flvPlayer.attachMediaElement(videoRef.current);
flvPlayer.unload();
flvPlayer.load();
const playPromise = flvPlayer.play();
if (playPromise !== undefined) {
console.log(' 😈😈', playPromise);
playPromise
.then(() => {
console.log('播放成功', flvPlayer);
})
.catch((e: any) => {
console.log('播放失败', e);
});
}
flvPlayer.on(flvjs.Events.STATISTICS_INFO, (info: any) => {
checkStuck(info);
});
flvPlayer.on(flvjs.Events.RECOVERED_EARLY_EOF, (info: any) => {
console.log('RECOVERED_EARLY_EOF', info);
});
// flvPlayer.on('error', err => {
// console.log('ERROR🤖', err)
// })
flvPlayer.on(flvjs.Events.ERROR, (err: any) => {
// flvPlayer.destroy()
console.log('flvjs.Events.ERROR👻', err);
if (count <= maxReloadCount) {
// 重连
rebuild();
} else {
if (onError) {
onError(err);
}
}
});
} else {
console.error('flv.js is not support');
}
} catch (error) {
console.log('trycatch😭', flvPlayer);
console.error(error);
}
};
function checkStuck(info: any) {
const { decodedFrames } = info;
let player = flvPlayer;
if (!player) return;
if (lastDecodedFrames === decodedFrames) {
// 可能卡住了,重载
stuckTime++;
console.log(`stuckTime${stuckTime},${new Date()}`);
if (stuckTime > 5) {
console.log(`%c卡住,重建视频`, 'background:red;color:#fff', new Date());
// 先destroy,再重建player实例
stuckTime = 0;
rebuild();
}
} else {
lastDecodedFrames = decodedFrames;
stuckTime = 0;
if (player && player?.buffered?.length > 0) {
let end = player.buffered.end(0); //获取当前buffered值(缓冲区末尾)
let delta = end - player.currentTime; //获取buffered与当前播放位置的差值
// 延迟过大,通过跳帧的方式更新视频
if (delta > 10 || delta < 0) {
console.log('延迟过大', delta);
player.currentTime = player.buffered.end(0) - 1; //
player.playbackRate = 1;
return;
}
// 追帧
if (delta > 1) {
console.log('追帧', delta);
player.playbackRate = 1.1;
} else {
player.playbackRate = 1;
}
// player.playbackRate = 1 + delta * 3
}
}
}
const rebuild = () => {
// 可以防止内存泄漏 摧毁重载一次整个flvjsplayer实例
try {
count++;
if (flvPlayer) {
console.log('😭触发重连操作', count);
flvPlayer?.pause();
flvPlayer?.unload();
flvPlayer?.detachMediaElement();
flvPlayer?.destroy();
flvPlayer = null;
init();
}
} catch (error) {
console.log('🤯这是rebuid的错误', error);
}
};
return (
<span className="relative">
<video
id="video"
ref={videoRef}
className={className}
style={style}
controls={controls}
autoPlay={autoPlay}
muted={muted}
height={height}
width={width}
{...videoProps}
/>
<Image
src={muted ? '/images/muted.png' : '/images/unmuted.png'}
width={24}
height={24}
alt=""
className=" absolute right-2 top-2"
onClick={() => {
setMuted(!muted);
}}
/>
</span>
);
};
export default FlvPlayer;
在其他组件中引入
引入时需要关闭服务端渲染,所以参考下面的方式引入,关闭ssr
/// index.ts文件
import dynamic from 'next/dynamic';
const FlvPlayer = dynamic(() => import('./FlvPlayer'), { ssr: false });
const Index=()=>{
return (
<div className="flex-cc">
<FlvPlayer url={url} type="flv" width={width} height={height} onError={showError} />
</div> );
}