学习链接
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"
]
}