关于前端JS将m3u8流合成MP4(h265格式)的记录

最近遇到的问题是将M3u8合成mp4,并下载下来。

初步定的方案是利用mux.js 获取m3u8的ts分片列表

然后通过分别下载ts成二进制流再合成,期间用到了。demuxer这个库,

但是实际测试下来,不是时间有问题就是有声音没有图像。

后来在网上找到了一个js库,m3u8-downloader大家可以去看看,其实也是用到了muxjs的方式,

按照这个库的方式写下来,发现了一个问题,就是他的示例下载没有问题,但是他的示例的ts分片数据是h264的,我们自己系统的ts分片是h265的。所以合成失败了

网上找了无数资源都没找到前端合成的方式。

后来在同事的共同努力下,终于找到了一个库。ffmpeg.wasm这个库。

https://github.com/ffmpegwasm/ffmpeg.wasmicon-default.png?t=N7T8https://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

具体示例代码在这里https://github.com/ffmpegwasm/ffmpeg.wasm/tree/main/apps/vue-vite-appicon-default.png?t=N7T8https://github.com/ffmpegwasm/ffmpeg.wasm/tree/main/apps/vue-vite-app

唯一要说的就是,如果要转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

好的,以下是一个简单的示例代码,供您参考: 1. 在 pom.xml 文件中添加 FFmpeg 的依赖库: ```xml <dependency> <groupId>com.github.kokorin.jaffree</groupId> <artifactId>jaffree</artifactId> <version>0.3.3</version> </dependency> ``` 2. 编写一个 HTML 页面,包含一个输入框和一个按钮,用户可以在输入框中输入 m3u8 地址,点击按钮后触发转换和下载操作。 ```html <!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>Convert M3U8 to MP4</title> </head> <body> <h1>Convert M3U8 to MP4</h1> <p>Input the M3U8 URL:</p> <input type="text" id="inputUrl"> <button onclick="convert()">Convert and Download</button> <p id="progress"></p> <script> function convert() { var url = document.getElementById("inputUrl").value; var xhr = new XMLHttpRequest(); xhr.open("POST", "/convert?url=" + encodeURIComponent(url), true); xhr.upload.onprogress = function(e) { var percent = Math.round(e.loaded / e.total * 100); document.getElementById("progress").innerText = "Conversion progress: " + percent + "%"; } xhr.onreadystatechange = function() { if (xhr.readyState === 4 && xhr.status === 200) { var fileName = xhr.responseText; var a = document.createElement("a"); a.href = "/download?fileName=" + encodeURIComponent(fileName); a.download = fileName; a.click(); } } xhr.send(); } </script> </body> </html> ``` 3. 在后端编写一个控制器,接收用户提交的 m3u8 地址,并使用 FFmpeg 提取 ts 碎片并将其合并为单个 mp4 文件。同时,可以使用 FFmpeg 的输出来获取合成进度,并将其传递给前端。 ```java @RestController public class ConverterController { @PostMapping("/convert") public ResponseEntity<String> convert(@RequestParam("url") String url) throws IOException { // Step 1: Parse the M3U8 file and get the list of ts files List<String> tsFiles = parseM3U8(url); // Step 2: Use FFmpeg to merge the ts files into a single MP4 file String fileName = mergeTsFiles(tsFiles); return ResponseEntity.ok(fileName); } private List<String> parseM3U8(String url) throws IOException { List<String> tsFiles = new ArrayList<>(); URL m3u8Url = new URL(url); BufferedReader reader = new BufferedReader(new InputStreamReader(m3u8Url.openStream())); String line; while ((line = reader.readLine()) != null) { if (line.endsWith(".ts")) { tsFiles.add(line); } } reader.close(); return tsFiles; } private String mergeTsFiles(List<String> tsFiles) throws IOException { FFmpeg ffmpeg = new FFmpeg("/path/to/ffmpeg"); // Replace with the actual path to FFmpeg FFprobe ffprobe = new FFprobe("/path/to/ffprobe"); // Replace with the actual path to FFprobe int numFiles = tsFiles.size(); List<Input> inputs = new ArrayList<>(numFiles); for (int i = 0; i < numFiles; i++) { String tsFile = tsFiles.get(i); inputs.add(Input.fromPath(tsFile)); } String fileName = "output.mp4"; Output output = new Builder(fileName) .setFormat("mp4") .addExtraArgs("-c", "copy") .done(); FFmpegExecutor executor = new FFmpegExecutor(ffmpeg, ffprobe); executor.createJob(inputs, output) .setProgressListener(new ProgressListener() { @Override public void progress(Progress progress) { // Send the progress to the client // The progress object contains information about the current frame number and the total number of frames // You can use this information to calculate the percentage of completion int percent = (int) (progress.getFrame() * 100 / progress.getTotalFrames()); // You can use web sockets or other techniques to send the progress to the client in real time // In this example, we just use a simple HTTP response to update the progress on the page return ResponseEntity.ok(percent); } }) .run(); return fileName; } @GetMapping("/download") public ResponseEntity<byte[]> download(@RequestParam("fileName") String fileName) throws IOException { Path file = Paths.get(fileName); byte[] bytes = Files.readAllBytes(file); HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_OCTET_STREAM); headers.setContentDisposition(ContentDisposition.attachment().filename(fileName).build()); return new ResponseEntity<>(bytes, headers, HttpStatus.OK); } } ``` 4. 在前端使用 AJAX 技术向后端发送请求,并通过进度条来显示合成进度。 5. 最后,将生成的 mp4 文件下载到指定目录。 这只是一个粗略的示例代码,实际实现还需要根据具体需求进行修改和优化。例如,如何处理 FFmpeg 的输出,如何在后台进行文件下载等等。建议您在实现过程中参考相关的文档和教程,并进行适当的修改和优化。
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值