1. 什么是 Jessibuca
Jessibuca 是一个基于 web 端的 HTML5 视频播放器库 ,专门用来播放实时或点播的 FLV/RTMP/WebRTC 视频流,无需额外插件。
它的核心特点是:
- 纯前端解码:通过 JavaScript + WebAssembly 实现视频解码。
- 跨浏览器支持:Chrome、Firefox、Edge 等现代浏览器均可使用。
- 高性能:支持硬件加速和多路视频播放。
- 开源:可以根据需求修改或封装功能。
2. 支持的视频流类型
Jessibuca 支持的流类型主要有:
|----------------------------------------|---------------------------------|
| 流类型 | 描述 |
| FLV(Flash Video) | 通过 HTTP 或 WebSocket 获取 FLV 流 |
| RTMP(Real-Time Messaging Protocol) | 主要用于直播推流(通常后台服务器转成 FLV) |
| HLS(HTTP Live Streaming) | 基于分段的直播或点播,Jessibuca 也能支持部分 HLS |
| WebRTC | 实时低延迟视频通道,适合监控或会议 |
在前端大多用的是 WebSocket FLV 流,因为无需 Flash,延迟低,兼容性好。
3. 工作原理(核心流程)
- 获取视频流:
后端推送 FLV 数据到 WebSocket 或 HTTP。
前端 Jessibuca 通过new Jessibuca({url, el})
接收。 - 解析视频数据
FLV 包含 H.264 视频帧和 AAC 音频帧。
Jessibuca 将二进制流解析成帧数据。 - 渲染视频:
使用 HTML5<canvas>
或<video>
元素渲染。
支持多路并发播放,通过独立实例管理。 - 控制操作:
暂停/播放:player.pause()
/player.play()
静音/声音:player.mute()
/player.unmute()
全屏:player.requestFullscreen()
截图:player.capturePicture()
返回 base64 图像
4. 使用场景与优势
Jessibuca 最常用的场景主要有:
|----------|----------------------------|
| 场景 | 说明 |
| 视频监控 | 安防摄像头实时视频展示,多路同时播放 |
| 远程会议 | WebRTC 或低延迟直播流显示 |
| 在线教育 | 老师推流,学生端实时观看 |
| 直播平台 | FLV/WebRTC 直播流播放 |
| 视频回放 | 将点播视频流转换为可播放的 Canvas/Video |
优势:
- 前端解码:无需服务器转码到 MP4 或 HLS,减少延迟。
- 支持多路并发:可同时播放多路视频而互不干扰。
- 灵活封装:可以和 Vue、React、Angular 等框架结合。
- 接口简单:提供播放、暂停、截图、静音、全屏等 API。
5. 在 vue 中如何使用 Jessibuca
Jessibuca 官网 也有很多 demo
5.1. 下载文件
首先去官网下载 jessibuca.js、 decoder.js、decoder.wasm

放到 public 下

5.2. index.html 引入
然后在 index.html 引入
html
<script type="text/javascript" src="/js/jessibuca/jessibuca.js"></script>
5.3. 封装 vue 文件
用 Vue 3 封装了一个 Jessibuca 播放器组件 。使用时只需传入 播放地址 和 是否显示控件 两个参数,就能快速集成视频播放功能。
此外,我还封装了 播放、暂停、截图、静音、全屏 等自定义操作,让视频交互更灵活,适合多路视频播放场景。
html
<template>
<div ref="container" style="width:100%; height: 100%; background-color: #000000;margin:0 auto;position: relative;"
@dblclick="fullscreenSwich">
<div style="width:100%; padding-top: 56.25%; position: relative;" />
</div>
<div id="buttonsBox" class="buttons-box" v-if="showButton">
<div class="buttons-box-left">
<el-icon v-if="!playing" @click="playBtnClick" title="播放">
<VideoPlay />
</el-icon>
<el-icon v-if="playing" @click="pause" title="暂停">
<VideoPause />
</el-icon>
<el-icon @click="destroy" title="停止">
<SwitchButton />
</el-icon>
<el-icon v-if="isNotMute" @click="mute" title="静音">
<MuteNotification />
</el-icon>
<el-icon v-if="!isNotMute" @click="cancelMute" title="取消静音">
<Bell />
</el-icon>
</div>
<div class="buttons-box-right">
<span class="jessibuca-btn">{{ kBps }} kb/s</span>
<el-icon class="jessibuca-btn" style="font-size: 1rem !important" @click="screenshot" title="截图">
<Camera />
</el-icon>
<el-icon class="jessibuca-btn" @click="playBtnClick" title="刷新">
<Refresh />
</el-icon>
<el-icon v-if="!fullscreen" class="jessibuca-btn" @click="fullscreenSwich" title="全屏">
<FullScreen />
</el-icon>
<el-icon v-if="fullscreen" class="jessibuca-btn" @click="fullscreenSwich" title="退出全屏">
<Rank />
</el-icon>
</div>
</div>
</template>
<script setup>
import { ref, onMounted, onBeforeUnmount, nextTick } from 'vue';
import { useRoute } from 'vue-router';
// import { ElMessage } from 'element-plus';
import { Camera, Bell, MuteNotification, VideoPlay, VideoPause, SwitchButton, Refresh, FullScreen, Rank } from '@element-plus/icons-vue';
const props = defineProps({
videoUrl: String,
showButton: { type: Boolean, default: true },
hasAudio: { type: Boolean, default: true },
showButton: { type: Boolean, default: true },
});
const emit = defineEmits(['playStatusChange', 'playTimeChange']);
const container = ref(null);
// ---- Vue 3 ref 管理播放器实例 ----
const jessibucaPlayer = ref(null);
const playing = ref(false);
const isNotMute = ref(true);
const quieting = ref(false);
const fullscreen = ref(false);
const loaded = ref(false); // loaded 代表是否加载完成
const performance = ref('');
const kBps = ref(0);
const playerTime = ref(0);
const currentVideoUrl = ref('');
const route = useRoute();
// --- 创建 Jessibuca 实例 ---
const create = () => {
if (!window.Jessibuca) {
console.error('[Jessibuca] window.Jessibuca 未找到,请检查 script 是否加载。');
return;
}
const options = {
container: container.value,
videoBuffer: 0,
isResize: true,
useMSE: true,
useWCS: false,
text: '',
hotKey: true,
debug: false, // 是否开启调试模式,开启后会有更多的日志输出
hasAudio: props.hasAudio,
isNotMute: isNotMute.value,
forceNoOffscreen: true,
loadingTimeout: 10,
keepScreenOn: true,
loadingTimeoutReplay: true,
loadingTimeoutReplayTimes: 3,
operateBtns: {
fullscreen: false,
screenshot: false,
play: false,
audio: false,
recorder: false,
},
loadingText: '请稍等, 视频加载中......',
};
try {
jessibucaPlayer.value = new window.Jessibuca(options);
const jessibuca = jessibucaPlayer.value;
// 监听播放事件
jessibuca.on('play', () => {
playing.value = true;
loaded.value = true;
});
// 监听暂停事件
jessibuca.on('pause', () => {
console.log('pause');
playing.value = false;
loaded.value = false;
});
jessibuca.on("kBps", (value) => {
kBps.value = Math.round(value)
})
// 监听停止事件
jessibuca.on('stop', () => {
playing.value = false;
loaded.value = false
});
// 监听播放结束事件
jessibuca.on('end', () => { })
jessibuca.on('fullscreen', (msg) => {
fullscreen.value = msg;
});
jessibuca.on('mute', (msg) => {
isNotMute.value = !msg;
});
jessibuca.on('performance', (val) => {
if (val === 2) performance.value = '非常流畅';
else if (val === 1) performance.value = '流畅';
else performance.value = '卡顿';
});
} catch (err) {
console.error('[Jessibuca] new Jessibuca 报错:', err);
jessibucaPlayer.value = null;
}
};
const createPlayer = () => {
return new Promise((resolve) => {
create();
// 这里监听 play 或者 nextTick 确保播放器初始化完成
nextTick(() => resolve());
});
};
// --- 播放 ---
const playBtnClick = () => {
play(currentVideoUrl.value);
};
const play = async (url) => {
currentVideoUrl.value = url;
// 等待销毁完成
await destroyPlayer();
// 等待创建完成
await createPlayer();
// 播放视频
if (jessibucaPlayer.value) {
jessibucaPlayer.value.play(currentVideoUrl.value);
}
};
// --- 暂停 ---
const pause = () => {
if (jessibucaPlayer.value) jessibucaPlayer.value.pause();
};
// --- 销毁 ---
const destroy = () => {
if (jessibucaPlayer.value) jessibucaPlayer.value.destroy();
jessibucaPlayer.value = null;
playing.value = false;
loaded.value = false;
performance.value = '';
playerTime.value = 0;
};
const destroyPlayer = () => {
return new Promise((resolve) => {
if (jessibucaPlayer.value) {
destroy();
// nextTick 确保 DOM 更新完成再 resolve
nextTick(() => resolve());
} else {
resolve();
}
});
};
// --- 截图 ---
const screenshot = () => {
if (jessibucaPlayer.value) jessibucaPlayer.value.screenshot();
};
// --- 静音 / 取消静音 ---
const mute = () => {
if (jessibucaPlayer.value) jessibucaPlayer.value.mute();
};
const cancelMute = () => {
if (jessibucaPlayer.value) jessibucaPlayer.value.cancelMute();
};
// --- 全屏切换 ---
const fullscreenSwich = () => {
if (!jessibucaPlayer.value) return;
const isFull = document.fullscreenElement ||
document.msFullscreenElement ||
document.mozFullScreenElement ||
document.webkitFullscreenElement;
jessibucaPlayer.value.setFullscreen(!isFull);
fullscreen.value = !isFull;
};
// --- 生命周期 ---
onMounted(() => {
nextTick(() => {
if (!props.videoUrl && route.params.url) {
currentVideoUrl.value = decodeURIComponent(route.params.url);
} else {
currentVideoUrl.value = props.videoUrl || '';
}
if (currentVideoUrl.value) play(currentVideoUrl.value);
});
});
onBeforeUnmount(() => {
destroy();
});
</script>
<style>
.buttons-box {
width: 100%;
height: 28px;
background-color: rgba(43, 51, 63, 0.7);
position: absolute;
/* 保持绝对定位 */
display: flex;
justify-content: space-between;
left: 0;
bottom: 0;
user-select: none;
z-index: 999;
/* 保证在播放器之上 */
}
.jessibuca-btn {
width: 20px;
color: #fff;
line-height: 27px;
margin: 0px 10px;
padding: 0 2px;
cursor: pointer;
text-align: center;
font-size: 0.8rem !important;
}
.buttons-box-right {
position: absolute;
right: 0;
}
</style>
组件使用:
html
<Player v-if="video" :videoUrl="video.ws_flv" :showButton="true" />