springboot-ffmpeg-m3u8-convertor nplayer视频播放弹幕效果

学习链接

ffmpeg-cli-wrapper - 内部封装了操作ffmpeg命令的java类库,它提供了一些类和方法,可以方便地构建和执行 ffmpeg 命令,而不需要直接操作字符串或进程。并且支持异步执行和进度监听

springboot-ffmpeg-m3u8-convertor - gitee代码 - springboot+ffmpeg,将视频转换为 m3u8 格式。支持 .mp4 | .flv | .avi | .mov | .wmv | .wav 格式视频转换。转换方式有:指定文件路径 、文件上传转换两种转换方式。

java-ffmpeg-convert-wav-to-mp3-demo

在 java 中使用 ffmpeg 的四个阶段

nplayer官网文档

artplayer官网文档

nplayer播放效果

在这里插入图片描述

代码

App.vue

<template>
  <div id="app">
    <div class="video-area">
      <div class="video-wrapper" ref="videoWrapperRef">
      </div>
      <div class="video-select-wrapper">
        <ul>
          <li v-for="(m3u8, index) in m3u8List" :key="index" @click="switchVideo(m3u8)">{{ m3u8.videoName }}</li>
        </ul>
      </div>
    </div>
    <div>
      <button @click="sendADanmu">发送1个弹幕</button>
      <button @click="pauseDanmu">暂停弹幕</button>
      <button @click="resumeDanmu">恢复弹幕</button>
      <button @click="getDanmu">获取弹幕列表</button>
      <!-- 添加一个弹幕到弹幕列表,并返回该弹幕插入下标。(大量弹幕请不要循环调用该方法,请使用其他批量方法) -->
      <button @click="addDanmuToDanmuList">添加一个弹幕到弹幕列表</button>
      <!-- 在现有弹幕列表末尾添加弹幕列表。需要保证添加的弹幕列表是有序的,而且其第一个弹幕的时间比现有的最后一个时间大。 -->
      <button @click="addDanmuListToDanmuList">在现有弹幕列表末尾添加弹幕列表</button>
      <!-- 重置弹幕列表。如果你又有一堆无序弹幕列表需要加入。可以通过 getItems() 获取现有弹幕,然后拼接两个列表,做排序,再调用该方法。 -->
      <button @click="resetDanmuList">重置弹幕列表</button>
    </div>
    <div>
      <button @click="startVideo">播放视频</button>
      <button @click="pauseVideo">暂停视频</button>
      <button @click="toggleVideo">切换视频播放状态</button>
      <button @click="jumpTo">跳到指定时间开始播放</button>
      <button @click="updatePlayerOptions">更新播放器参数</button>
      <button @click="destroyPlayer">销毁</button>

    </div>
  </div>
</template>

<script>
import Hls from 'hls.js'
import Player, { EVENT } from 'nplayer';
import Danmaku from '@nplayer/danmaku'
console.log(EVENT)

export default {
  name: 'App',
  components: {
  },
  data() {
    return {
      hls: null,
      player: null,
      video: null,
      danmaku: null,
      m3u8List: [
        { videoId: '1', videoUrl: 'http://127.0.0.1/test3.m3u8', poster: 'http://127.0.0.1/20250219/13/41/poster.jpg', videoName: '第1集' },
        { videoId: '2', videoUrl: 'https://test-streams.mux.dev/x36xhzz/x36xhzz.m3u8', poster: '', videoName: '联网视频' },
        { videoId: '3', videoUrl: 'http://127.0.0.1/test1.m3u8', poster: 'http://127.0.0.1/20250219/13/37/poster.jpg', videoName: '第2集' },
        { videoId: '4', videoUrl: 'http://127.0.0.1/test5.m3u8', poster: '', videoName: '第3集' },
        { videoId: '5', videoUrl: 'http://127.0.0.1/test6.m3u8', poster: '', videoName: '第4集' },
        // {videoUrl: 'http://127.0.0.1/20250219/13/49/test7.m3u8',poster:'http://127.0.0.1/poster.jpg',videoName: '第5集'},
      ]
    }
  },
  mounted() {
    this.initPlayer(/* this.m3u8List[0] */)
  },
  methods: {
    isNativeHlsSupported() {
      const video = document.createElement("video");
      return video.canPlayType("application/vnd.apple.mpegurl") !== "";
    },
    destroyPlayer() {
      if (this.player) {
        if (this.player.playing) {
          this.player.pause()
        }
        this.player.danmaku.resetItems([])
        this.player.dispose();
      }
      if (this.hls) {
        this.hls.destroy()
      }
    },
    switchVideo(m3u8) {
      console.log('播放: ', m3u8);
      this.destroyPlayer()
      this.initPlayer(m3u8)
    },
    sendADanmu() {
      /* 这里不需要设置time时间,默认直接取视频时间 */
      this.danmaku.send({ text: 'sendADanmu~~~' })
    },
    pauseDanmu() {
      console.log(this.danmaku.paused);
      this.danmaku.pause()
      console.log(this.danmaku.paused);
    },
    resumeDanmu() {
      console.log(this.danmaku.paused);
      this.danmaku.resume()
      console.log(this.danmaku.paused);
    },
    getDanmu() {
      console.log(this.danmaku.getItems());
    },
    addDanmuToDanmuList() {
      // 添加一个弹幕到弹幕列表,并返回该弹幕插入下标。(大量弹幕请不要循环调用该方法,请使用其他批量方法)
      // 这里需要设置time时间,否则不会添加到弹幕列表
      console.log(this.danmaku.addItem({ text: 'addDanmuToDanmuList~~~', time: 8 }));
    },
    addDanmuListToDanmuList() {
      /* 它会在末尾添加大量弹幕,这里可以不指定time */
      console.log(this.danmaku.appendItems(
        [
          { text: 'addDanmuListToDanmuList~~~' },
          { text: 'addDanmuListToDanmuList~~~' },
          { text: 'addDanmuListToDanmuList~~~' },
          { text: 'addDanmuListToDanmuList~~~' },
          { text: 'addDanmuListToDanmuList~~~' },
        ]
      ));
    },
    resetDanmuList() {
      const oldItems = this.player.danmaku.getItems()
      const newUnsortItems = [
        { time: 3, text: '重置弹幕1' },
        { time: 4, text: '重置弹幕2' },
        { time: 10, text: '重置弹幕3' },
      ]
      const sortedItems = oldItems.concat(newUnsortItems).sort((a, b) => a.time - b.time)
      this.player.danmaku.resetItems(sortedItems)
    },
    startVideo() {
      this.player.play()
    },
    pauseVideo() {
      this.player.pause()
    },
    toggleVideo() {
      this.player.toggle()
    },
    jumpTo() {
      this.player.seek(30)
    },
    updatePlayerOptions() {
      this.player.updateOptions({})
    },
    initPlayer(m3u8) {
      console.log('initPlayer' + m3u8);
      this.video = document.createElement('video')
      const videoWrapper = this.$refs.videoWrapperRef
      let _this = this
      const danmakuOptions = {
        // 是否开启无限弹幕模式。
        unlimited: true,
        // 1、弹幕列表必须按照 time 从小到大排序。
        //    如果获取的弹幕是无序的,那么在传入之前需要自己 .sort((a, b) => a.time - b.time) 一下。
        // 2、你还可以通过 danmaku 对象的 appendItems 和 resetItems 等方法,添加和重置弹幕。
        items: [],
        // 发送弹幕之前会调用该回调,用来判断是否丢弃当前弹幕。
        discard(bullet) {
          console.log('discard bullet? 内容是:', bullet.text, bullet);
          if (bullet.text.indexOf('2B') > -1) {
            // 不显示该弹幕
            return true
          } else {
            return false
          }
        }
      }

      this.player = new Player({
        isTouch: false, // 默认会自动检测
        poster: m3u8?.poster,
        // 开启快捷键功能
        shortcut: true,
        // volumeStep 参数控制
        volumeStep: 0.2,
        // 前进或后退 时长参数控制
        seekStep: 2,
        // videoProps: { autoplay: false },
        volumeVertical: true,
        i18n: 'zh',
        video: this.video,
        plugins: [
          new Danmaku(danmakuOptions)
        ]
      })
      window.player = this.player
      // 弹幕插件还会在 player 对象上注册一个 danmaku 对象。可以通过 player.danmaku 访问该对象。
      // 可以通过 danmaku 对象的 appendItems 和 resetItems 等方法,添加和重置弹幕
      console.log('danmaku 对象', this.player.danmaku)
      this.danmaku = this.player.danmaku
      window.danmaku = this.player.danmaku
      // 用户发送弹幕之前触发。
      this.player.on('DanmakuSend', (opts) => {
        console.log('DanmakuSend', opts);
      })
      this.player.on("DanmakuUpdateOptions", (opts) => {
        console.log('DanmakuUpdateOptions', opts);
      })
      this.player.mount(videoWrapper)

      if (this.isNativeHlsSupported()) {
        this.player.src = url; // 直接使用原生播放
      } else {
        if (m3u8 && m3u8.videoUrl) {
          this.hls = new Hls()
          this.hls.attachMedia(this.player.video)
          this.hls.on(Hls.Events.MEDIA_ATTACHED, function () {
            console.log('Hls.Events.MEDIA_ATTACHED监听');
            // _this.hls.loadSource('https://test-streams.mux.dev/x36xhzz/x36xhzz.m3u8')
            // _this.hls.loadSource('http://127.0.0.1/20250219/13/41/test3.m3u8')
            // _this.hls.loadSource('http://127.0.0.1/test/test.m3u8')
          })
          this.hls.on(Hls.Events.MANIFEST_PARSED, function () {
            console.log('Hls.Events.MANIFEST_PARSED监听');
          })
          this.hls.loadSource(m3u8.videoUrl)
          // 模拟加载弹幕
          new Promise((resolve, reject) => {
            console.log('加载弹幕');
            this.player.danmaku.appendItems([
              { time: 1, text: m3u8.videoId + '- 弹幕1~' },
              { time: 2, text: m3u8.videoId + '- 弹幕2~' },
              { time: 2.5, text: m3u8.videoId + '- 2B~' },
              { time: 3, text: m3u8.videoId + '- 弹幕3~' },
              { time: 4, text: m3u8.videoId + '- 弹幕4~' },
              { time: 5, text: m3u8.videoId + '- 弹幕5~' },
              { time: 6, text: m3u8.videoId + '- 自定义弹幕哦~', color: '#f00', type: 'scroll', isMe: false, force: true }
            ])
          })
        }
      }

      window.hls = this.hls
    },

  },


}
</script>

<style lang="scss">
body {
  margin: 0;
}
.nplayer_video {
  object-fit: cover;
}
.video-area {
  width: 1200px; // 80 * 45   16 9 
  margin: 20px auto;
  display: flex;
  height: 448px;
}

.video-wrapper {
  width: 800px;
  border: 1px solid #ccc;
  border-radius: 6px;
  overflow: hidden;
  box-shadow: 0 5px 10px 2px rgba(0,0,0,.08);
}

ul,
li {
  list-style-type: none;
  padding: 0;
  margin: 0;
}

.video-select-wrapper {
  border: 1px solid #ccc;
  border-radius: 4px;
  margin-left: 15px;
  flex: 1;

  ul {
    width: 100%;
    height: 100%;

    li {
      border: 1px solid #eee;
      margin: 5px;
      height: 30px;
      text-align: center;
      line-height: 30px;
      color: #333;

      &:hover {
        background-color: #8cc6f2;
        border-radius: 4px;
        cursor: pointer;
        color: #fff;
      }
    }
  }
}

button {
  margin: 5px;
}
</style>


package.json

{
  "name": "nplayer-demo",
  "version": "0.1.0",
  "private": true,
  "scripts": {
    "serve": "vue-cli-service serve",
    "build": "vue-cli-service build"
  },
  "dependencies": {
    "@nplayer/danmaku": "^1.0.12",
    "core-js": "^3.8.3",
    "hls.js": "^1.5.20",
    "nplayer": "^1.0.15",
    "vue": "^2.6.14"
  },
  "devDependencies": {
    "@vue/cli-plugin-babel": "~5.0.0",
    "@vue/cli-service": "~5.0.0",
    "sass": "^1.32.7",
    "sass-loader": "^12.0.0",
    "vue-template-compiler": "^2.6.14"
  },
  "browserslist": [
    "> 1%",
    "last 2 versions",
    "not dead"
  ]
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/972502.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

在做题中学习(90):螺旋矩阵II

解法&#xff1a;模拟 思路&#xff1a;创建相同大小的一个二维数组&#xff08;矩阵&#xff09;&#xff0c;用变量标记原矩阵的行数和列数&#xff0c;每次遍历完一行或一列&#xff0c;相应行/列数--&#xff0c;进行对应位置的赋值即可。此题是正方形矩阵&#xff0c;因此…

FreeRTOS任务调度介绍

FreeRTOS 操作系统支持三种调度方式:抢占式调度,时间片调度和合作式调度。 实际应用主要是抢占式调度和时间片调度,合作式调度用到的很少 (1)抢占式调度 每个任务都有不同的优先级,任务会一直运行直到被高优先级任务抢占或者遇到阻塞式的 API 函数,比如vTaskDelay,执…

微信小程序image组件mode属性详解

今天学习微信小程序开发的image组件&#xff0c;mode属性的属性值不少&#xff0c;一开始有点整不明白。后来从网上下载了一张图片&#xff0c;把每个属性都试验了一番&#xff0c;总算明白了。现总结归纳如下&#xff1a; 1.使用scaleToFill。这是mode的默认值&#xff0c;sc…

关于Unity的一些基础知识点汇总

1.Prefab实例化后&#xff0c;哪些资源是共用的&#xff1f;哪些资源是拷贝的&#xff1f; 共用资源 脚本组件&#xff1a;实例化后的 Prefab 共享脚本组件的代码。若脚本中无状态数据&#xff0c;多个实例对脚本方法的调用会有相同逻辑。比如一个控制物体移动的脚本&#xff0…

React之旅-02 创建项目

创建React项目&#xff0c;常用的方式有两种&#xff1a; 官方提供的脚手架&#xff0c;官网&#xff1a;https://create-react-app.dev/。如需创建名为 my-app 的项目&#xff0c;请运行如下命令&#xff1a; npx create-react-app my-app 使用Vite包&#xff0c;官网&…

Cursor实战:Web版背单词应用开发演示

Cursor实战&#xff1a;Web版背单词应用开发演示 需求分析自行编写需求文档借助Cursor生成需求文档 前端UI设计后端开发项目结构环境参数数据库设计安装Python依赖运行应用 前端代码修改测试前端界面 测试数据生成功能测试Bug修复 总结 在上一篇《Cursor AI编程助手不完全指南》…

【JavaEE进阶】Spring MVC(3)

欢迎关注个人主页&#xff1a;逸狼 创造不易&#xff0c;可以点点赞吗 如有错误&#xff0c;欢迎指出~ 返回响应 返回静态页面 //RestController Controller RequestMapping("/response") public class ResponseController {RequestMapping("/returnHtmlPage&…

文本操作基础知识:正则表达式

目录 摘要&#xff1a; 一、语法 二、匹配模式pattern 1、普通字符[ ] 2、限定字符 3、定位字符 4、运算字符( ) 三、修饰符flags 四、各语言的正则使用 1、Python的re 参考资料&#xff1a; 摘要&#xff1a; 常用匹配&#xff1a;[A-C]、[^A-C]、\w、\d、\n、\r、…

ROS-相机话题-获取图像-颜色目标识别与定位-目标跟随-人脸检测

文章目录 相机话题获取图像颜色目标识别与定位目标跟随人脸检测 相机话题 启动仿真 roslaunch wpr_simulation wpb_stage_robocup.launch rostopic hz /kinect2/qhd/image_color_rect/camera/image_raw&#xff1a;原始的、未经处理的图像数据。 /camera/image_rect&#xff…

Ubuntu USB耳机找不到设备解决

​ 一. 确定硬件连接 lsusb -t 插拔USB耳机&#xff0c;确定是否有USB识别到 二. 查看输出设备 sudo apt-get install pavucontrol pavucontrol 点击想要使用的输出设备后面的绿色选项 三. 输出设备没有USB耳机时调试 3.1 确认ALSA是否识别设备 列出ALSA播放设备&#…

深入解析「卡顿帧堆栈」 | UWA GPM 2.0 技术细节与常见问题

在游戏开发过程中&#xff0c;卡顿问题一直是影响玩家体验的关键因素。UWA GPM 2.0全新推出的「卡顿帧堆栈」功能&#xff0c;专为研发团队提供精准、高效的卡顿分析方案&#xff0c;能够直观呈现游戏运行时的堆栈信息&#xff0c;助力团队迅速找到性能瓶颈。该功能一经上线&am…

Web3.py 入门笔记

Web3.py 学习笔记 &#x1f4da; 1. Web3.py 简介 &#x1f31f; Web3.py 是一个 Python 库&#xff0c;用于与以太坊区块链进行交互。它就像是连接 Python 程序和以太坊网络的桥梁。 官方文档 1.1 主要功能 查询区块链数据&#xff08;余额、交易等&#xff09;发送交易与…

点击unity资源文件自动展开左侧的文件路径

背景&#xff1a; 最近从cocos那边转过来的unity同事总是吐糟我们unity选中一个资源后都无法清晰的看到他的文件路径&#xff0c;这给他的工作带来了很多的烦恼&#xff0c;于是我想到昨天刚看到一个unity编辑器下的简易协程实现&#xff0c;通过2个接口Selection.activeObjec…

几种查询本机公网IP的方式

英文网站 bgp.he.net 链接地址:https://bgp.he.net/ bgp.he.net是一个在线工具平台,主要用于查询IP的路由信息,特别是与BGP(边界网关协议)相关的信息。 以下是对bgp.he.net的详细介绍: 一、平台功能 BGP查询:用户可以通过输入IP地址,查询该IP的BGP路由信息,包括AS号…

每日一题——编辑距离

编辑距离 参考资料题目描述示例 解题思路动态规划&#xff08;DP&#xff09;方法 代码实现复杂度分析示例详解示例1&#xff1a;"nowcoder" → "new"示例2&#xff1a;"intention" → "execution" 总结与心得 参考资料 建议先参考下…

ChatGPT行业热门应用提示词案例-AI绘画类

AI 绘画指令是一段用于指导 AI 绘画工具&#xff08;如 DALLE、Midjourney 等&#xff09;生成特定图像的文本描述。它通常包含场景、主体、风格、色彩、氛围等关键信息&#xff0c;帮助 AI 理解创作者的意图&#xff0c;从而生成符合要求的绘画作品。 ChatGPT 拥有海量的知识…

LearnOpenGL——高级OpenGL(下)

教程地址&#xff1a;简介 - LearnOpenGL CN 高级数据 原文链接&#xff1a;高级数据 - LearnOpenGL CN 在OpenGL中&#xff0c;我们长期以来一直依赖缓冲来存储数据。本节将深入探讨一些操作缓冲的高级方法。 OpenGL中的缓冲本质上是一个管理特定内存块的对象&#xff0c;它…

VScode插件EIDE - 嵌入式开发工具

Embedded IDE - 可以选开源GCC编译器&#xff0c;直接替代Keil&#xff1b;或者用Keil内置的编译器&#xff0c; - 可导入keil的工程&#xff0c;与Keil Assistant插件相比&#xff0c;优势在于可以不用打开Keil改文件架构&#xff08;增删等&#xff09; 再吐槽一下富文本编辑…

C语言进阶——6-C语言文件操作

目录 本章重点1. 为什么使用文件2. 什么是文件2.1 程序文件2.2 数据文件2.3 文件名 3. 文件的打开和关闭3.1 文件指针3.2 文件的打开和关闭 4. 文件的顺序读写4.1 顺序写4.2 顺序读4.3 文本行输入函数——写一行数据4.4 文本行输出函数——读一行数据4.5 格式化写入文件4.6 格式…

2025年人工智能与教育系统国际学术会议(ICAIES 2025)

重要信息 大会官网&#xff1a; www.icispp.com 大会时间&#xff1a;2025年3月14-16日 大会地点&#xff1a;中国-北京 简介 会议主题主要围绕教育创新与多媒体技术等相关研究领域展开讨论&#xff0c;旨在为相关领域的专家学者及企业发展人提供一个分享研究成果、讨论存…