Jessibuca 播放器

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. 工作原理(核心流程)

  1. 获取视频流:
    后端推送 FLV 数据到 WebSocket 或 HTTP。
    前端 Jessibuca 通过 new Jessibuca({url, el}) 接收。
  2. 解析视频数据
    FLV 包含 H.264 视频帧和 AAC 音频帧。
    Jessibuca 将二进制流解析成帧数据。
  3. 渲染视频:
    使用 HTML5 <canvas><video> 元素渲染。
    支持多路并发播放,通过独立实例管理。
  4. 控制操作:
    暂停/播放: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" />
相关推荐
晴殇i21 小时前
Web端PDF预览方法详解
前端·javascript·vue.js
加油乐21 小时前
解决 iOS 端输入框聚焦时页面上移问题
前端·javascript·ios
鹏多多21 小时前
纯前端提取图片颜色插件Color-Thief教学+实战完整指南
前端·javascript·vue.js
Moment21 小时前
Soul 发布超强端侧语音模型,没错,就是你想的那个 Soul 😍😍😍
前端·后端·github
井柏然21 小时前
重识 alias —— npm包开发的神器
前端·javascript·前端工程化
Mintopia21 小时前
🤖 AIGC在Web教育场景中的自适应学习技术设计
前端·javascript·aigc
Mintopia21 小时前
⚙️ Next.js 多环境部署全攻略
前端·javascript·全栈
cngm11021 小时前
若依分离版前端部署在tomcat刷新404的问题解决方法
java·前端·tomcat
摸鱼的春哥21 小时前
组合为啥比继承更高级?以构建buff系统为例
前端·javascript·后端
江城开朗的豌豆21 小时前
让TS函数"说到做到":返回值类型约束的实战心得
前端·javascript