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" />
相关推荐
超级神性造梦机器3 小时前
当“提示词工程”过时,谁来帮开发者管好 AI 的“注意力”?
前端·后端
吃饺子不吃馅3 小时前
小明问:要不要加入创业公司?
前端·面试·github
不渡_3 小时前
Web项目-版本号
前端·javascript
Asort3 小时前
JavaScript设计模式(十一):享元模式(Flyweight) - 优化内存与性能的利器
前端·javascript·设计模式
Asort3 小时前
JavaScript设计模式(十)——外观模式 (Facade)
前端·javascript·设计模式
创码小奇客3 小时前
前端小白从零到一:架构师视角下的学习路线与实战指南
前端·javascript·架构
星链引擎3 小时前
智能聊天机器人落地指南 场景案例、代码集成与优化策略
前端
我是天龙_绍3 小时前
ES6 Class 类的基本语法
前端
掘金安东尼3 小时前
⏰前端周刊第435期(2025年10月6日–10月12日)
前端·javascript·github