前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >120 行代码实现纯 Web 剪辑视频

120 行代码实现纯 Web 剪辑视频

作者头像
winty
发布2021-09-17 11:30:38
8490
发布2021-09-17 11:30:38
举报
文章被收录于专栏:前端Q

前几天偶尔看到一篇 webassembly 的相关文章,对这个技术还是挺感兴趣的,在了解一些相关知识的基础上,看下自己能否小小的实践下。

什么是 webasembly?

WebAssembly(wasm)就是一个可移植、体积小、加载快并且兼容 Web 的全新格式。可以将 C,C++等语言编写的模块通过编译器来创建 wasm 格式的文件,此模块通过二进制的方式发给浏览器,然后 js 可以通过 wasm 调用其中的方法功能。

WebAssembly 的优势

网上对于这个相关的介绍应该有很多了,WebAssembly 优势性能好,运行速度远高于 Js,对于需要高计算量、对性能要求高的应用场景如图像/视频解码、图像处理、3D/WebVR/AR 等,优势非常明显,们可以将现有的用 C、C++等语言编写的库直接编译成 WebAssembly 运行到浏览器上,并且可以作为库被 JavaScript 引用。那就意味着我们可以将很多后端的工作转移到前端,减轻服务器的压力。.........

WebAssembly 最简单的实践调用

我们编写一个最简单的 c 文件

代码语言:javascript
复制
int add(int a,int b) {
  return a + b;
}

然后安装对于的 Emscripten 编译器Emscripten 安装指南

代码语言:javascript
复制
emcc test.c -Os -s WASM=1 -s SIDE_MODULE=1 -o test.wasm

然后我们在 html 中引入使用即可

代码语言:javascript
复制
fetch('./test.wasm').then(response =>
  response.arrayBuffer()
).then(bytes =>
  WebAssembly.instantiate(bytes)
).then(results => {
  const add = results.instance.exports.add
  console.log(add(11,33))
});

这时我们即可在控制台看到对应的打印日志,成功调用我们编译的代码啦

正式开动

既然我们已经知道如何能快速的调用到一些已经成熟的 C,C++的类库,那我们离在线剪辑视频预期目标更进一步了。

最终 demo 演示

由于录制操作的电脑 cpu 不太行,所以可能耗时比较久,但整体的效果还是能看的到滴

demo 仓库地址(https://github.com/Dseekers/clip-video-by-webassembly)

FFmpeg

在这个之前你得稍微的了解下啥是 FFmpeg? 以下根据维基百科的目录解释

FFmpeg 是一个开放源代码的自由软件,可以运行音频和视频多种格式的录影、转换、流功能[1],包含了 libavcodec——这是一个用于多个项目中音频和视频的解码器库,以及 libavformat——一个音频与视频格式转换库。

简单的说这个就是由 C 语言编写的视频处理软件,它的用法也是相当滴简单

我主要将这次需要用到的命令给调了出来,如果你还可能用到别的命令,可以根据他的官方文档查看 ,还可以了解下阮一峰大佬的文章 (https://www.ruanyifeng.com/blog/2020/01/ffmpeg.html)

代码语言:javascript
复制
ffmpeg -ss [start] -i [input] -to [end] -c copy [output]

start 为开始时间 end 为结束时间 input 为需要操作的视频源文件 output 为输出文件的位置名称

这一行代码就是我们需要用到的剪辑视频的命令了

获取相关的FFmpeg的wasm

由于通过 Emscripten 编译 ffmpeg 成 wasm 存在较多的环境问题,所以我们这次直接使用在线已经编译好的 CDN 资源

这边就直接使用了这个比较成熟的库 https://github.com/ffmpegwasm/ffmpeg.wasm

为了本地调试方便,我把其相关的资源都下了下来 一共 4 个资源文件

代码语言:javascript
复制
ffmpeg.min.js
ffmpeg-core.js
ffmpeg-core.wasm
ffmpeg-core.worker.js

我们使用的时候只需引入第一个文件即可,其它文件会在调用时通过 fetch 方式去拉取资源

最小的功能实现

前置功能实现: 在我们本地需要实现一个 node 服务,因为使用 ffmpeg 这个模块会出现如果没在服务器端设置响应头, 会报错 SharedArrayBuffer is not defined,这个是因为系统的安全漏洞,浏览器默认禁用了该 api,若要启用则需要在 header 头上设置

代码语言:javascript
复制
Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: require-corp

我们启动一个简易的 node 服务

代码语言:javascript
复制
const Koa = require('koa');
const path = require('path')
const fs = require('fs')
const router = require('koa-router')();
const static = require('koa-static')
const staticPath = './static'
const app = new Koa();
app.use(static(
    path.join(__dirname, staticPath)
))
// log request URL:
app.use(async (ctx, next) => {
    console.log(`Process ${ctx.request.method} ${ctx.request.url}...`);
    ctx.set('Cross-Origin-Opener-Policy', 'same-origin')
    ctx.set('Cross-Origin-Embedder-Policy', 'require-corp')
    await next();
});

router.get('/', async (ctx, next) => {
    ctx.response.body = '<h1>Index</h1>';
});
router.get('/:filename', async (ctx, next) => {
    console.log(ctx.request.url)
    const filePath = path.join(__dirname, ctx.request.url);
    console.log(filePath)
    const htmlContent = fs.readFileSync(filePath);
    ctx.type = "html";
    ctx.body = htmlContent;
});
app.use(router.routes());
app.listen(3000);
console.log('app started at port 3000...');

我们做一个最小化的 demo 来实现下这个剪辑功能,剪辑视频的前一秒钟 新建一个 demo.html 文件,引入相关资源

代码语言:javascript
复制
<script src="https://cdn.jsdelivr.net/npm/jquery@3.6.0/dist/jquery.min.js"></script>
<script src="./assets/ffmpeg.min.js"></script>

<div class="container">
  <div class="operate">
    选择原始视频文件:
    <input type="file" id="select_origin_file">
    <button id="start_clip">开始剪辑视频</button>
  </div>
  <div class="video-container">
    <div class="label">原视频</div>
    <video class="my-video" id="origin-video" controls></video>
  </div>
  <div class="video-container">
    <div class="label">处理后的视频</div>
    <video class="my-video" id="handle-video" controls></video>
  </div>
</div>
代码语言:javascript
复制
let originFile
$(document).ready(function () {
  $('#select_origin_file').on('change', (e) => {
    const file = e.target.files[0]
    originFile = file
    const url = window.webkitURL.createObjectURL(file)
    $('#origin-video').attr('src', url)
  })
  $('#start_clip').on('click', async function () {
    const { fetchFile, createFFmpeg } = FFmpeg;
    ffmpeg = createFFmpeg({
      log: true,
      corePath: './assets/ffmpeg-core.js',
    });
    const file = originFile
    const { name } = file;
    if (!ffmpeg.isLoaded()) {
      await ffmpeg.load();
    }
    ffmpeg.FS('writeFile', name, await fetchFile(file));
    await ffmpeg.run('-i', name, '-ss', '00:00:00', '-to', '00:00:01', 'output.mp4');
    const data = ffmpeg.FS('readFile', 'output.mp4');
    const tempURL = URL.createObjectURL(new Blob([data.buffer], { type: 'video/mp4' }));
    $('#handle-video').attr('src', tempURL)
  })
});

其代码的含义也是相当简单,通过引入的 FFmpeg 去创建一个实例,然后通过 ffmpeg.load()方法去加载对应的 wasm 和 worker 资源 没有进行优化的 wasm 的资源是相当滴大,本地文件竟有 23MB,这个若是需要投入生产的可是必须通过 emcc 调节打包参数的方式去掉无用模块。然后通 fetchFile 方法将选中的 input file 加载到内存中去,接下来就可以通过 ffmpeg.run 运行和 本地命令行一样的 ffmpeg 命令行参数了参数基本一致

这时我们的核心功能已经实现完毕了。

做一点小小的优化

剪辑的话最好是可以选择时间段,我这为了方便直接把 element 的以 cdn 方式引入使用 通过 slider 来截取视频区间,我这边就只贴 js 相关的代码了,具体代码可以去 github 仓库里面仔细看下

代码语言:javascript
复制
class ClipVideo {
    constructor() {
        this.ffmpeg = null
        this.originFile = null
        this.handleFile = null
        this.vueInstance = null
        this.currentSliderValue = [0, 0]
        this.init()
    }
    init() {
        console.log('init')
        this.initFfmpeg()
        this.bindSelectOriginFile()
        this.bindOriginVideoLoad()
        this.bindClipBtn()
        this.initVueSlider()
    }
    initVueSlider(maxSliderValue = 100) {
        console.log(`maxSliderValue ${maxSliderValue}`)
        if (!this.vueInstance) {
            const _this = this
            const Main = {
                data() {
                    return {
                        value: [0, 0],
                        maxSliderValue: maxSliderValue
                    }
                },
                watch: {
                    value() {
                        _this.currentSliderValue = this.value
                    }
                },
                methods: {
                    formatTooltip(val) {
                        return _this.transformSecondToVideoFormat(val);
                    }
                }
            }
            const Ctor = Vue.extend(Main)
            this.vueInstance = new Ctor().$mount('#app')
        } else {
            this.vueInstance.maxSliderValue = maxSliderValue
            this.vueInstance.value = [0, 0]
        }
    }
    transformSecondToVideoFormat(value = 0) {
        const totalSecond = Number(value)
        let hours = Math.floor(totalSecond / (60 * 60))
        let minutes = Math.floor(totalSecond / 60) % 60
        let second = totalSecond % 60
        let hoursText = ''
        let minutesText = ''
        let secondText = ''
        if (hours < 10) {
            hoursText = `0${hours}`
        } else {
            hoursText = `${hours}`
        }
        if (minutes < 10) {
            minutesText = `0${minutes}`
        } else {
            minutesText = `${minutes}`
        }
        if (second < 10) {
            secondText = `0${second}`
        } else {
            secondText = `${second}`
        }
        return `${hoursText}:${minutesText}:${secondText}`
    }
    initFfmpeg() {
        const { createFFmpeg } = FFmpeg;
        this.ffmpeg = createFFmpeg({
            log: true,
            corePath: './assets/ffmpeg-core.js',
        });
    }
    bindSelectOriginFile() {
        $('#select_origin_file').on('change', (e) => {
            const file = e.target.files[0]
            this.originFile = file
            const url = window.webkitURL.createObjectURL(file)
            $('#origin-video').attr('src', url)

        })
    }
    bindOriginVideoLoad() {
        $('#origin-video').on('loadedmetadata', (e) => {
            const duration = Math.floor(e.target.duration)
            this.initVueSlider(duration)
        })
    }
    bindClipBtn() {
        $('#start_clip').on('click', () => {
            console.log('start clip')
            this.clipFile(this.originFile)
        })
    }
    async clipFile(file) {
        const { ffmpeg, currentSliderValue } = this
        const { fetchFile } = FFmpeg;
        const { name } = file;
        const startTime = this.transformSecondToVideoFormat(currentSliderValue[0])
        const endTime = this.transformSecondToVideoFormat(currentSliderValue[1])
        console.log('clipRange', startTime, endTime)
        if (!ffmpeg.isLoaded()) {
            await ffmpeg.load();
        }
        ffmpeg.FS('writeFile', name, await fetchFile(file));
        await ffmpeg.run('-i', name, '-ss', startTime, '-to', endTime, 'output.mp4');
        const data = ffmpeg.FS('readFile', 'output.mp4');
        const tempURL = URL.createObjectURL(new Blob([data.buffer], { type: 'video/mp4' }));
        $('#handle-video').attr('src', tempURL)
    }
}
$(document).ready(function () {
    const instance = new ClipVideo()
});

这样文章开头的效果就这样实现啦

小结

webassbembly 还是比较新的一项技术,我这边只是应用了其中一小部分功能,值得我们探索的地方还有很多,欢迎大家多多交流哈

参考资料

  • WebAssembly 完全入门——了解 wasm 的前世今生 (https://juejin.cn/post/6844903709806182413)
  • 使用 FFmpeg 与 WebAssembly 实现纯前端视频截帧 (https://toutiao.io/posts/7as4kva/preview)
  • 前端视频帧提取 ffmpeg + Webassembly (https://juejin.cn/post/6854573219454844935)

前往微医互联网医院在线诊疗平台,快速问诊,3分钟为你找到三甲医生。(https://wy.guahao.com/?channel=influence)

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2021-09-10,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 前端Q 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 什么是 webasembly?
  • WebAssembly 的优势
  • WebAssembly 最简单的实践调用
  • 正式开动
  • 最终 demo 演示
  • FFmpeg
  • 获取相关的FFmpeg的wasm
  • 最小的功能实现
  • 做一点小小的优化
  • 小结
  • 参考资料
相关产品与服务
内容分发网络 CDN
内容分发网络(Content Delivery Network,CDN)通过将站点内容发布至遍布全球的海量加速节点,使其用户可就近获取所需内容,避免因网络拥堵、跨运营商、跨地域、跨境等因素带来的网络不稳定、访问延迟高等问题,有效提升下载速度、降低响应时间,提供流畅的用户体验。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档