服务端源代码 github.com/gowvp/gb28181
前端源代码 github.com/gowvp/gb28181_web
介绍
GoWVP (Golang Web Video Platfrom) 是一个 Go 语言实现的,基于 GB28181-2022 标准实现的网络视频平台,负责实现核心信令与设备管理后台部分,支持海康、大华、宇视等品牌的 IPC、NVR、DVR 接入。支持国标级联,支持rtsp/rtmp等视频流转发到国标平台,支持 rtsp/rtmp 等推流转发到国标平台。
技术栈
Golang v1.23, Goweb v1.x, Gin v1.10, Gorm v1.25 …
React 19, Vite 6.x, Typescript, React-Router v7, React-Query v5, shadcn/ui …
先看效果图
打开 jessibuca 开源项目,下载 dist.zip
文件,将文件解压缩拷贝的项目目录 public/assets/js/
下。
react-router v7
在是 root.tsx
文件中定义 Layout
函数,其返回了 HTML,截图中所示引用了环境变量,因为我们项目部署后有个前缀目录,注意别落下。
在 dist.zip
中存在 jessibuca.d.ts
文件,拷贝到 components/player
目录下,返回 div
标签,等会我们将播放器挂载到该标签里。
interface PlayerProps {
ref: React.RefObject;
link: string; // 播放的流地址
}
export default function Player({ ref,url }: PlayerProps) {
const divRef = useRef(null);
return ;
}
使用 useEffect 初始化 Jessibuca,Jessibuca.Config
是刚刚复制过来 jessibuca.d.ts
文件中定义的类型,ts
类属性语法提示很好用。
在初始化过程中最重要的四点
decoder
一定要指定准确的位置,否则找不到解码器会播放黑屏 useEffect(() => {
// 播放器已经初始化,无需再次执行
if (p.current) {
return;
}
const cfg: Jessibuca.Config = {
container: divRef.current!,
// 注意,这里很重要!! 加载解码器的路径
decoder: `${import.meta.env.VITE_BASENAME}assets/js/decoder.js`,
debug: true,
useMSE: true,
isNotMute: true,
showBandwidth: true, // 显示带宽
loadingTimeout: 7, // 加载地址超时
heartTimeout: 7, // 没有流数据,超时
videoBuffer: 0.2,
isResize: true,
operateBtns: {
fullscreen: true,
screenshot: true,
play: true,
audio: true,
record: true,
},
};
p.current = new window.Jessibuca(cfg);
// 如果传入了播放链接,在加载播放器以后就可以播放了
if (link) {
play(link);
}
return () => {
console.log(" ~ Jessibuca-player ~ dispose");
};
}, []);
window.Jessibuca(cfg)
会提示 window 没有 Jessibuca
这个函数,我们定义一个。
declare global {
interface Window {
Jessibuca: any;
}
}
在上面初始化完成后,执行的 play
函数,用于播放流。
const play = (link: string) => {
console.log(" Jessibuca-player ~ play ~ link:", link);
if (!p.current) {
console.log(" Jessibuca-player ~ play ~ 播放器未初始化:");
toastError({ title: "播放器未初始化" });
return;
}
if (!p.current.hasLoaded()) {
console.log(" Jessibuca-player ~ play ~ 播放器未加载完成:");
toastError({ title: "播放器未加载完成" });
return;
}
p.current
.play(link)
.then(() => {
console.log(" Jessibuca-player ~ play ~ success");
})
.catch((e) => {
toastError({ title: "播放失败", description: e.message });
});
};
还需要提供一个销毁函数,避免页面关闭后,播放器还在后台消耗资源。
const destroy = () => {
console.log(" Jessibuca-player ~ play destroy");
if (p.current) {
p.current.destroy();
p.current = null;
}
};
控制反转是一种软件设计原则,它将对象的控制权从调用者转移到另一个对象或框架。
简单说一说
正常是组件控制自己的状态,或者父组件中定义状态,传递给子组件用。
控制反转是指在子组件中定义了状态,但将状态控制权交给了父组件,每个引用子组件的父组件就不需要定义那么多状态属性。
在 react 19 以前,子组件需要 forwardRef
函数接收 ref
,那是旧时代的东西啦,该项目使用的正是 React 19.x,直接将 ref 作为参数传递即可。
通过 useImperativeHandle
将子组件的控制权交出去,也就是上面我们定义的函数。
useImperativeHandle(ref, () => ({
play, // 播放
destroy, // 销毁
}));
第五步 应用播放组件
playerRef
是播放器的控制器,用于调用 play
和 destroy
。
link
是流连接,如果不传递此参数,需要主动调用 playerRef.current?.play(link)
来播放。
父组件的生命周期结束(即页面销毁时) 一定要调用playerRef.current?.destroy()
避免播放器还在后台消耗资源。
设置个最小宽高避免窗口缩放时,播放器变形,这就搞定了。
export type PlayerRef = {
play: (link: string) => void;
destroy: () => void;
};
// ......
const playerRef = useRef(null);
// 流地址
const [link, setLink] = useState("");
// 关闭弹窗,并销毁播放器
const close = () => {
setOpen(false);
playerRef.current?.destroy();
};
//.........
{/* 播放器设置一个最小宽高 */}
随着业务需求,播放器组件可以提供更多的控制函数,交由父组件调用。
总结
写代码很简单,除非它很难。如果它很难,写代码未必简单。
React 19 ref as a prop 官方文档
React useImperativeHandle 官方文档
播放器 github.com/langhuihui/jessibuca