最近遇到的问题是将M3u8合成mp4,并下载下来。
初步定的方案是利用mux.js 获取m3u8的ts分片列表
然后通过分别下载ts成二进制流再合成,期间用到了。demuxer这个库,
但是实际测试下来,不是时间有问题就是有声音没有图像。
后来在网上找到了一个js库,m3u8-downloader大家可以去看看,其实也是用到了muxjs的方式,
按照这个库的方式写下来,发现了一个问题,就是他的示例下载没有问题,但是他的示例的ts分片数据是h264的,我们自己系统的ts分片是h265的。所以合成失败了
网上找了无数资源都没找到前端合成的方式。
后来在同事的共同努力下,终于找到了一个库。ffmpeg.wasm这个库。
https://github.com/ffmpegwasm/ffmpeg.wasmhttps://github.com/ffmpegwasm/ffmpeg.wasm
具体适用方法呢?里面有案例我就不详细讲了
给那些没有梯子的人大致的讲一下
整体代码:
<template>
<video :src="video" controls />
<br />
<button @click="transcode">Start</button>
<p>{{ message }}</p>
</template>
<script lang="ts">
import { FFmpeg } from '@ffmpeg/ffmpeg'
import type { LogEvent } from '@ffmpeg/ffmpeg/dist/esm/types'
import { fetchFile, toBlobURL } from '@ffmpeg/util'
import { defineComponent, ref } from 'vue'
const baseURL = 'https://unpkg.com/@ffmpeg/core-mt@0.12.6/dist/esm'
const videoURL = 'https://raw.githubusercontent.com/ffmpegwasm/testdata/master/video-15s.avi'
export default defineComponent({
name: 'App',
setup() {
const ffmpeg = new FFmpeg()
const message = ref('Click Start to Transcode')
let video = ref('')
async function transcode() {
message.value = 'Loading ffmpeg-core.js'
ffmpeg.on('log', ({ message: msg }: LogEvent) => {
message.value = msg
})
await ffmpeg.load({
coreURL: await toBlobURL(`${baseURL}/ffmpeg-core.js`, 'text/javascript'),
wasmURL: await toBlobURL(`${baseURL}/ffmpeg-core.wasm`, 'application/wasm'),
workerURL: await toBlobURL(`${baseURL}/ffmpeg-core.worker.js`, 'text/javascript')
})
message.value = 'Start transcoding'
await ffmpeg.writeFile('test.avi', await fetchFile(videoURL))
await ffmpeg.exec(['-i', 'test.avi', 'test.mp4'])
message.value = 'Complete transcoding'
const data = await ffmpeg.readFile('test.mp4')
video.value = URL.createObjectURL(new Blob([(data as Uint8Array).buffer], { type: 'video/mp4' }))
}
return {
video,
message,
transcode
}
}
})
</script>
<style>
#app {
font-family: Avenir, Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #2c3e50;
margin-top: 60px;
}
</style>
安装方式:
npm install @ffmpeg/ffmpeg @ffmpeg/util
or
yarn add @ffmpeg/ffmpeg @ffmpeg/util
or
pnpm install @ffmpeg/ffmpeg @ffmpeg/util
我使用的版本是@ffmpeg/ffmpeg 0.12.10 @ffmpeg/util 0.12.1
唯一要说的就是,如果要转m3u8,首先下载m3u8下的ts列表,完成以后放到一个数组里面,
再循环
// 以三个ts片段为例videoURL1 videoURL2 videoURL3 为上一步m3u8获取的ts分片地址
await ffmpeg.writeFile('test1.ts', await fetchFile(videoURL1))
await ffmpeg.writeFile('test2.ts', await fetchFile(videoURL2))
await ffmpeg.writeFile('test3.ts', await fetchFile(videoURL3))
// 这是默认的
await ffmpeg.exec(['-i', 'test.ts', 'test.mp4'])
// 这是修改后的 这里是重点命令!!!!!!!!!
await ffmpeg.exec(['-i', 'concat:test1.ts|test2.ts|test3.ts', 'test.mp4'])
还有一个踩坑点,使用它的代码的时候一定会报某个错ShareArrayBuffer is not defined(这玩意而是跨域隔离策略导致的)
const baseURL = 'https://unpkg.com/@ffmpeg/core-mt@0.12.6/dist/esm'
await ffmpeg.load({
coreURL: await toBlobURL(`${baseURL}/ffmpeg-core.js`, 'text/javascript'),
wasmURL: await toBlobURL(`${baseURL}/ffmpeg-core.wasm`, 'application/wasm'),
workerURL: await toBlobURL(`${baseURL}/ffmpeg-core.worker.js`, 'text/javascript')
})
首先把这三个下载下来,放到vue3的public文件里面,本地访问。
// 部署以后得Nginx配置
location / {
absolute_redirect on;
port_in_redirect on;
#autoindex on;
root /opt/local/web/dist;
index index.html index.htm;
add_header Cross-Origin-Embedder-Policy require-corp;
add_header Cross-Origin-Opener-Policy same-origin;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $host;
}
add_header Cross-Origin-Embedder-Policy require-corp;
add_header Cross-Origin-Opener-Policy same-origin;
这两行是必须的,也就是说必须打开同源策略,可能会导致引用其他源的资源出错,比如高德地图不显示logo,不显示定位图层等等,这个可能要单独处理了。
这只是前端合成m3u8的一种方法,如果允许还是让后端做转换把
其他的坑我就不填了,比如跨域。AES加密解密之类的,自己解决一下了
最后贴一段完整下载代码,仅供参考!(baseURL请换成自己的地址,测试的时候可以先用固定地址),这段下载代码,是做了分块处理,每100ts个合成一个视频
const downloadMp4 = async (m3u8Url, filename = `${+new Date()}`, callback) => {
callback && callback({
message: `正在请求视频数据源`,
length: 0,
current: 0,
done: false
})
const response = await fetch(m3u8Url).then(response => response.text())
const tsUrlList = [];
response.split('\n').forEach(item => {
// if (/.(png|image|ts|jpg|mp4|jpeg)/.test(item)) {
// 放开片段后缀限制,下载非 # 开头的链接片段
if (/^[^#]/.test(item)) {
tsUrlList.push(item)
}
})
callback && callback({
message: `视频数据源请求完成`,
length: tsUrlList.length,
current: 0,
done: false
});
async function transcode() {
// 将 tsUrlList 分割成多个子数组
const chunkSize = 100;
const chunks = [];
let downloadNumber = 0;
for (let i = 0; i < tsUrlList.length; i += chunkSize) {
chunks.push(tsUrlList.slice(i, i + chunkSize));
}
// 对每个子数组进行处理
for (let i = 0; i < chunks.length; i++) {
const ffmpeg = new FFmpeg()
ffmpeg.on('log', ({ message: msg }) => {
console.log(msg)
})
const baseURL = '/main';
callback && callback({
message: `正在加载合成依赖`,
length: tsUrlList.length,
current: downloadNumber,
done: false
})
await ffmpeg.load({
coreURL: await toBlobURL(`${baseURL}/ffmpeg-core.js`, 'text/javascript'),
wasmURL: await toBlobURL(`${baseURL}/ffmpeg-core.wasm`, 'application/wasm'),
workerURL: await toBlobURL(`${baseURL}/ffmpeg-core.worker.js`, 'text/javascript')
})
// await ffmpeg.writeFile('test.ts', await fetchFile('/main/123.ts'))
// await ffmpeg.exec(['-i', 'test.ts', 'test.mp4'])
callback && callback({
message: `合成依赖加载完成`,
length: tsUrlList.length,
current: downloadNumber,
done: false
})
const chunk = chunks[i];
let execStr = `concat:`;
for (let j = 0; j < chunk.length; j++) {
const element = chunk[j];
let response;
downloadNumber ++;
try {
response = await fetchFile(element);
} catch (error) {
console.error('下载失败:', error);
continue; // 如果下载失败,跳过当前循环,进入下一个循环
}
await ffmpeg.writeFile(`test${i * chunkSize + j}.ts`, response)
execStr += `test${i * chunkSize + j}.ts|`;
callback && callback({
message: `正在下载`,
length: tsUrlList.length,
current: downloadNumber,
done: false
})
}
callback && callback({
message: `视频合成中`,
length: tsUrlList.length,
current: downloadNumber,
done: false
})
console.log('execStrexecStrexecStrexecStr=>', execStr.substring(0, execStr.length-1))
await ffmpeg.exec(['-i', execStr.substring(0, execStr.length-1), '-c','copy',`test${i}.mp4`])
// 删除已经处理过的TS文件
// for (let j = 0; j < chunk.length; j++) {
// const filename = `test${i * chunkSize + j}.ts`;
// ffmpeg.deleteFile(filename);
// }
let data = await ffmpeg.readFile(`test${i}.mp4`)
let src = URL.createObjectURL(new Blob([data.buffer], { type: 'video/mp4' }))
callback && callback({
message: `视频合成完成,启动下载`,
length: tsUrlList.length,
current: i === chunks.length - 1 ? tsUrlList.length : downloadNumber,
done: i === chunks.length - 1 ? true : false,
})
let link = document.createElement('a');
link.href = src;
link.download = `${filename}_${i}.mp4`; // 这里设置你想要的文件名
link.click(); // 这将开始下载
link.remove(); // 移除 'a' 元素
ffmpeg.terminate();
}
}
transcode();
}
使用方式:
downloadMp4(hasNeedPlaying.url, filename, (res) => {
// 这里是下载过程中的回调函数
message.value[index] = res;
res.done && hasDone++;
if (sum === hasDone) {
// 当前视频下载完成,开始下载下一个
downloadOneByOne(index + 1);
}
});
视频
QQ202467-101730