WebAssembly002 FFmpegWasmLocalServer项目

项目介绍

  • https://github.com/incubated-geek-cc/FFmpegWasmLocalServer.git可将音频或视频文件转换为其他可选的多媒体格式,并导出转码的结果
$ bash run.sh 
FFmpeg App is listening on port 3000!

运行效果

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

相关依赖

Error: Cannot find module ‘express’

  • npm install express
$npm install express
npm WARN old lockfile 
npm WARN old lockfile The package-lock.json file was created with an old version of npm,
npm WARN old lockfile so supplemental metadata must be fetched from the registry.
npm WARN old lockfile 
npm WARN old lockfile This is a one-time fix-up, please be patient...
npm WARN old lockfile 
npm WARN EBADENGINE Unsupported engine {
npm WARN EBADENGINE   package: 'FFmpegWasmLocalServer@1.0.0',
npm WARN EBADENGINE   required: { node: '12.13.0' },
npm WARN EBADENGINE   current: { node: 'v16.20.0', npm: '8.19.4' }
npm WARN EBADENGINE }
npm WARN deprecated consolidate@0.16.0: Please upgrade to consolidate v1.0.0+ as it has been modernized with several long-awaited fixes implemented. Maintenance is supported by Forward Email at https://forwardemail.net ; follow/watch https://github.com/ladjs/consolidate for updates and release changelog

added 70 packages, and audited 71 packages in 2m

7 packages are looking for funding
  run `npm fund` for details

found 0 vulnerabilities

启动服务

// 引入 Express 库
const express = require('express');
// 创建一个 Express 应用程序实例
const app = express();
// 设置应用程序的端口号,使用环境变量 PORT 或默认值 3000
const PORT = process.env.PORT || 3000;

// 引入文件系统和路径处理模块
const fs = require('fs');
const path = require('path');
// 引入 Consolidate 模块用于模板引擎支持
const engine = require("consolidate");
// 引入 compression 模块用于启用响应压缩
const compression = require('compression');

// 使用 compression 中间件,对所有响应进行压缩
app.use(compression());

// 中间件,启用 SharedBuffer
app.use(function(req, res, next) {
  // 设置响应头,启用 SharedBuffer
  res.header("Cross-Origin-Embedder-Policy", "require-corp");
  res.header("Cross-Origin-Opener-Policy", "same-origin");
  // 继续执行下一个中间件或路由处理函数
  next();
});

// 静态文件中间件,将 public 目录设置为静态文件目录
app.use(express.static(path.join(__dirname, "public")))
// 设置视图目录为 views
.set("views", path.join(__dirname, "views"))
// 使用 Mustache 模板引擎
.engine("html", engine.mustache)
// 设置视图引擎为 Mustache
.set("view engine", "html")

// 处理根路径的 GET 请求,渲染 index.html 页面
.get("/", (req, res) => res.render("index.html"))
// 处理 /index.html 路径的 GET 请求,同样渲染 index.html 页面
.get("/index.html", (req, res) => res.render("index.html"))

// 监听指定的端口号,当应用程序启动时打印日志
.listen(PORT, () => {
  console.log(`FFmpeg App is listening on port ${PORT}!`);
});

https服务:

  • 非https访问可能存在如下问题:The Cross-Origin-Opener-Policy header has been ignored, because the URL’s origin was untrustworthy. It was defined either in the final response or a redirect. Please deliver the response using the HTTPS protocol. You can also use the ‘localhost’ origin instead. See https://www.w3.org/TR/powerful-features/#potentially-trustworthy-origin and https://html.spec.whatwg.org/#the-cross-origin-opener-policy-header.
const express = require('express');
const https = require('https');
const fs = require('fs');
const compression = require('compression');
const engine = require('consolidate');

const app = express();
const PORT = process.env.PORT || 3000;

// 1. 使用 Let's Encrypt 获取 SSL/TLS 证书,并将证书文件放置在项目中
// or just openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout server.key -out server.crt
const options = {
  key: fs.readFileSync('path/to/server.key'),
  cert: fs.readFileSync('path/to/server.crt'),
};

// 2. 配置 Express 应用程序以使用 HTTPS
const server = https.createServer(options, app);

// 3. 添加 HTTP 到 HTTPS 的重定向中间件
app.use(function (req, res, next) {
  if (!req.secure) {
    return res.redirect('https://' + req.headers.host + req.url);
  }
  next();
});

// 4. 添加其他中间件和路由
app.use(compression());
app.use(function (req, res, next) {
  res.header('Cross-Origin-Embedder-Policy', 'require-corp');
  res.header('Cross-Origin-Opener-Policy', 'same-origin');
  next();
});

app.use(express.static(__dirname + '/public'))
  .set('views', __dirname + '/views')
  .engine('html', engine.mustache)
  .set('view engine', 'html')
  .get('/', (req, res) => res.render('index.html'))
  .get('/index.html', (req, res) => res.render('index.html'));

// 启动 Express 应用程序
server.listen(PORT, () => {
  console.log(`FFmpeg App is listening on port ${PORT} with HTTPS!`);
});

index.html

<html lang='en' class='notranslate' translate='no'>
  <head>
      <!-- 设置页面元数据 -->
      <meta name='google' content='notranslate' />      <meta charset='UTF-8'>      <meta name='description' content='An Offline Multimedia File Conversion Tool.'>      <meta name='keywords' content='ffmpeg,wasm API,audio-conversion'>      <meta name="author" content="Charmaine Chui" />      <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">      <meta name='viewport' content='width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0' />     <meta http-equiv='Content-Language' content='en' />
      <title>Media Transcoder | Built With FFmpeg for Audio & Video Files</title>
      <meta name='msapplication-TileColor' content='#ffffff' />      <meta name='theme-color' content='#ffffff' />      <meta name='apple-mobile-web-app-status-bar-style' content='black-translucent' />      <meta name='apple-mobile-web-app-capable' content='yes' />      <meta name='mobile-web-app-capable' content='yes' />      <meta name='HandheldFriendly' content='True' />      <meta name='MobileOptimized' content='320' />

      <!-- 设置网站图标 -->
      <link rel="apple-touch-icon" sizes="76x76" href="img/favicon-76.png">      <link rel="apple-touch-icon" sizes="120x120" href="img/favicon-120.png">      <link rel="apple-touch-icon" sizes="152x152" href="img/favicon-152.png">      <link rel="icon" sizes="196x196" href="img/favicon-196.png">      <link rel="icon" type="image/x-icon" href="img/favicon.ico">

      <!-- 引入样式表 -->
      <link href='css/bootstrap-4.5.2.min.css' rel='stylesheet' type='text/css' />
      <link href='css/offcanvas.css' rel='stylesheet' type='text/css' />
      <link href='css/custom.css' rel='stylesheet' type='text/css' />
  </head>
  <!-- 在无法运行JavaScript的情况下显示提示信息 -->
  <noscript>You need to enable JavaScript to run this app.</noscript>
  <body>
    <!-- 网站导航栏 -->
    <nav id='site-header' class="navbar navbar-expand-sm bg-light navbar-light border-bottom fixed-top pt-0 pb-0 text-muted small">
        <!-- 网站标志 -->
        <!-- 网站信息和链接 -->
    </nav>

    <!-- 主体内容区域 -->
    <div class='container-full h-100 p-1'>
      <div class='row no-gutters'>
        <!-- 第一个列:选择输出文件格式 -->
        <div class='col-sm-4 p-1'>
          <!-- 卡片组件 -->
          <div class="card rounded-0">
            <div class="card-header p-1">
              <span class='symbol'>❶</span> Select media format of output file
            </div>
            <div class="card-body p-1">
              <!-- 表格组件 -->
              <table class='table table-bordered small mb-0 w-100'>
                <thead>
                  <tr>
                    <td colspan='2'>
                      <!-- 输入框和下拉列表 -->
                    </td>
                  </tr>
                </thead>
                <!-- 输出文件详细信息 -->
                      <!-- 重置按钮 -->
                      <button id='resetAllBtn' type='button' class='btn btn-sm btn-outline-danger rounded-circle navBtn float-right text-center symbol'>↺</button>

                    </td>
                  </tr>
                </tbody>
              </table>
            </div>
          </div>
        </div>

        <!-- 第二个列:上传媒体文件 -->
                      <!-- 上传文件按钮 -->
                      <button id='uploadMediaBtn' type='button' class='btn btn-sm btn-light border border-primary text-primary rounded-0'>
                        <span class='emoji'>📂</span> <small>Upload File</small><input id='uploadMedia' type='file' accept='audio/*,video/*' />
                      </button>
                </thead>
              
                <tbody>
				 <!-- 输入文件详细信息 -->
                </tbody>
                <tr>
                  <td colspan='2' valign='middle'>
                    <!-- 保存按钮 -->
                    <button id='saveOutput' type='button' class='btn btn-sm btn-outline-success rounded-circle navBtn float-right text-center symbol'>💾</button>
                    <span class='symbol float-right mr-2 text-success'>𝙴𝚡𝚙𝚘𝚛𝚝 𝙿𝚛𝚘𝚌𝚎𝚜𝚜𝚎𝚍 𝙾𝚞𝚝𝚙𝚞𝚝 ▷</span>
                  </td>
                </tr>
              </table>
            </div>
          </div>
        </div>

        <!-- 第三个列:服务器的 Cross-Origin Isolated 状态 -->
        <!-- 支持的文件格式信息 -->
             
              <!-- 媒体文件预览区域 -->
              <div id='mediaWrapper' class='text-center'></div>
            </div>
            
          </div>
        </div>
      </div>

      <!-- 底部输出日志区域 -->
      <div class='row no-gutters'>
    </div>

    <!-- 引入JavaScript文件 -->
    <script src='js/polyfill.js'></script>
    <script src='js/ie10-viewport-bug-workaround.js'></script>
    <script src='js/bootstrap-native-v4.js'></script>
    <script src="js/ffmpeg/ffmpeg.min.js"></script>
    <script src="js/mimeTypes.js"></script>
    <script src="js/custom.js"></script>
  </body>
</html>

脚本标签作用
<script src='js/polyfill.js'></script>JavaScript特性的兼容性支持,确保在旧版本的浏览器正常运行
<script src='js/ie10-viewport-bug-workaround.js'></script>解决在 Internet Explorer 10 (IE10) 浏览器中的一些视口(viewport)相关的问题
<script src='js/bootstrap-native-v4.js'></script>引入 Bootstrap 框架的 JavaScript 部分,提供页面布局、样式和交互的基本功能。
<script src="js/ffmpeg/ffmpeg.min.js"></script>引入 FFmpeg 库
<script src="js/mimeTypes.js"></script>定义和处理不同媒体类型(MIME类型)的脚本
<script src="js/custom.js"></script>自定义的 JavaScript 代码

custom.js

// 检查文档是否完全加载,如果是则立即执行回调,否则等待DOMContentLoaded事件
if (document.readyState === "complete" || document.readyState !== "loading" && !document.documentElement.doScroll) {
    callback();
} else {
    // 在DOMContentLoaded事件触发时执行
    document.addEventListener('DOMContentLoaded', async () => {
        console.log('DOMContentLoaded');

        // 获取所有类名为 'card' 的元素
        const cards = document.querySelectorAll('.card');
        let maxHeight;

        // 计算所有 'card' 元素的最大高度
        for (let card of cards) {
            if (typeof maxHeight === 'undefined' || card.clientHeight > maxHeight) {
                maxHeight = card.clientHeight;
            }
        }

        // 设置所有 'card' 元素的高度和溢出样式
        for (let card of cards) {
            card['style']['height'] = `${maxHeight}px`;
            card['style']['overflow-y'] = 'auto';
        }

        // 设置 logsOutput 元素的高度
        const logsOutput = document.getElementById('logsOutput');
        logsOutput['style']['height'] = `calc(100vh - 50px - 0.25rem - 0.25rem - 0.25rem - 0.25rem - 0.25rem - ${maxHeight}px)`;

        // 显示当前年份
        const yearDisplay = document.getElementById('yearDisplay');
        yearDisplay.innerHTML = new Date().getFullYear();

        // 获取 outputLogs 元素
        const outputLogs = document.getElementById('outputLogs');

        // 获取当前日期时间的字符串表示
        function getCurrentDatetimeStamp() {
            const d = new Date();
            let datestamp = d.getFullYear() + '-' + ((d.getMonth() + 1 < 10) ? ('0' + (d.getMonth() + 1)) : (d.getMonth() + 1)) + '-' + ((d.getDate() < 10) ? ('0' + d.getDate()) : (d.getDate()));
            let timestamp = ((d.getHours() < 10) ? ('0' + d.getHours()) : (d.getHours())) + ':' + ((d.getMinutes() < 10) ? ('0' + d.getMinutes()) : (d.getMinutes())) + ':' + ((d.getSeconds() < 10) ? ('0' + d.getSeconds()) : (d.getSeconds()));
            let datetimeStr = datestamp + ' ' + timestamp;
            return datetimeStr;
        }

        // 日志类型常量
        const infoNote = 'ɪɴғᴏ ';
        const errNote = 'ᴇʀʀᴏʀ';

        // 添加数据日志到页面
        function appendDataLog(logMsg) {
            if (typeof logMsg === 'string') {
                let logType = infoNote;
                let textClass = 'text-light bg-dark';

                // 根据日志内容判断日志类型,并设置样式
                if (logMsg.toLowerCase().includes('fail')) {
                    logType = errNote;
                    textClass = 'text-light bg-danger';
                    logMsg = `${logMsg}`;
                } else if (logMsg.indexOf('[fferr] size= ') === 0 || logMsg.indexOf('[fferr] frame= ') === 0) {
                    textClass = 'text-white bg-primary'; // 重要的操作需求
                    logMsg = `${logMsg}`;
                } else if (logMsg.indexOf('[fferr]') === 0 && logMsg.includes(':') && !logMsg.toLowerCase().includes('config')) {
                    textClass = 'text-primary bg-light'; // 文件信息
                    logMsg = `${logMsg}`;
                } else if (logMsg.indexOf('[info]') === 0) {
                    textClass = 'text-dark'; // 比填充更好
                    logMsg = `${logMsg}`;
                } else if (logMsg.indexOf('[fferr]') === 0) {
                    textClass = 'text-secondary'; // 填充日志
                    logMsg = `${logMsg}`;
                } else if (logMsg.indexOf('[ffout]') === 0) {
                    textClass = 'text-white bg-success'; // 重要通知,处理结束
                    logMsg = `${logMsg}`;
                } else {
                    logMsg = `${logMsg}`;
                }

                // 插入日志到页面
                outputLogs.insertAdjacentHTML('beforeend', '<p class="mb-0 small"><span class="unicode text-dark mr-1">' + logType + '</span><span class="text-white bg-dark"><span class="symbol">【</span>' + getCurrentDatetimeStamp() + '<span class="symbol">】</span></span> <span class="' + textClass + '"> ' + logMsg.trim() + ' </span></p>');

                // 滚动到日志底部
                let scrollTopVal = outputLogs.scrollHeight - outputLogs.clientHeight;
                outputLogs.scroll(0, scrollTopVal);
            }
        }

        // 添加错误日志到页面
        function appendErrorLog(errMsg) {
            if (typeof errMsg === 'string') {
                outputLogs.insertAdjacentHTML('beforeend', '<p class="mb-0 small"><span class="unicode text-dark mr-1">' + errNote + '</span><span class="text-white bg-dark"><span class="symbol">【</span>' + getCurrentDatetimeStamp() + '<span class="symbol">】</span></span> <span class="text-light bg-danger"> ' + errMsg.trim() + ' </span></p>');

                // 滚动到日志底部
                let scrollTopVal = outputLogs.scrollHeight - outputLogs.clientHeight;
                outputLogs.scroll(0, scrollTopVal);
            }
        }

        // 重写 console.log 和 console.error,将输出信息显示在页面中的 outputLogs 元素中
        console.logs = console.log.bind(console);
        console.log = function () {
            console.logs.apply(console, arguments);
            if (Array.from(arguments).length === 1 && typeof (Array.from(arguments)[0]) === 'string') {
                appendDataLog(Array.from(arguments)[0]);
            }
        };

        console.errors = console.error.bind(console);
        console.error = function () {
            console.errors.apply(console, arguments);
            if (Array.from(arguments).length === 1 && typeof Array.from(arguments) === 'object') {
                appendErrorLog(Array.from(arguments)[0].path[0].error.message);
            }
        };

检查跨域隔离是否生效

        // 检查是否为跨域隔离
        const isCrossOriginIsolated = document.getElementById('isCrossOriginIsolated');
        if (crossOriginIsolated) {
            isCrossOriginIsolated.innerHTML = '🟢'; // 绿色
        } else {
            isCrossOriginIsolated.innerHTML = '🔴'; // 红色
        }

关键元素

        // 上传文件相关元素
        const uploadMediaBtn = document.getElementById('uploadMediaBtn');
        const uploadMedia = document.getElementById('uploadMedia');

        // 文件信息展示元素
        const fileNameDisplay = document.getElementById('FileName');
        const fileTypeDisplay = document.getElementById('FileType');
        const fileSizeDisplay = document.getElementById('FileSize');

        // 输出文件信息展示元素
        const outputFileExtension = document.getElementById('outputFileExtension');
        const FileExtDisplay = document.getElementById('FileExt');
        const MimeTypeDisplay = document.getElementById('MimeType');
        const MimeDescriptionDisplay = document.getElementById('MimeDescription');

        // 重置和保存按钮
        const resetAllBtn = document.getElementById('resetAllBtn');
        const saveOutputBtn = document.getElementById('saveOutput');
        saveOutputBtn.disabled = true;

相关函数

        // 触发事件,重置按钮点击时调用
        function triggerEvent(el, type) {
            let e = (('createEvent' in document) ? document.createEvent('HTMLEvents') : document.createEventObject());
            if ('createEvent' in document) {
                e.initEvent(type, false, true);
                el.dispatchEvent(e);
            } else {
                e.eventType = type;
                el.fireEvent('on' + e.eventType, e);
            }
        }

        // Uint8Array 转为 Base64,上传文件时调用
        const convertBitArrtoB64 = (bitArr) => (btoa(bitArr.reduce((data, byte) => data + String.fromCharCode(byte), '')));

        // 读取文件为 Array Buffer,上传文件时调用
        function readFileAsArrayBuffer(file) {
            return new Promise((resolve, reject) => {
                let fileredr = new FileReader();
                fileredr.onload = () => resolve(fileredr.result);
                fileredr.onerror = () => reject(fileredr);
                fileredr.readAsArrayBuffer(file);
            });
        }

选中事件

        let isSelected = false;
        let counter = 0;

        // 填充文件类型下拉框
        for (let mimeTypeObj of mimeTypes) {
            let fileExt = mimeTypeObj['Extension'];
            let fileDescription = mimeTypeObj['Description'];
            let fileMimeType = mimeTypeObj['MIME_Types'][0];
            let conversionWorks = mimeTypeObj['Works'];

            let oOption = document.createElement('option');
            oOption.value = fileMimeType;
            oOption.text = `${fileDescription} [${fileExt}]`;

            if (!isSelected) {
                oOption.setAttribute('selected', true);
                MimeTypeDisplay.innerHTML = fileMimeType;
                FileExtDisplay.innerHTML = fileExt;
                MimeDescriptionDisplay.innerHTML = fileDescription;
                isSelected = true;
            }
            outputFileExtension.add(oOption, counter++);
        }

        // 延时处理,确保页面加载完成
        await new Promise((resolve, reject) => setTimeout(resolve, 50));

        // 文件类型下拉框选择变化事件
        outputFileExtension.addEventListener('change', async (e) => {
            let allOptions = e.currentTarget.options;
            let optionSelectedIndex = e.currentTarget.selectedIndex;
            let mimeType = allOptions[optionSelectedIndex].value;

            let fileExtStr = ((e.currentTarget.options[optionSelectedIndex].textContent).split('[')[1]);
            fileExtStr = fileExtStr.replaceAll(']', '');

            let mimeDescriptionStr = ((e.currentTarget.options[optionSelectedIndex].textContent).split('[')[0]);
            mimeDescriptionStr = mimeDescriptionStr.trim();

            MimeTypeDisplay.innerHTML = mimeType;
            FileExtDisplay.innerHTML = fileExtStr;
            MimeDescriptionDisplay.innerHTML = mimeDescriptionStr;
        });

        // HTML5 兼容的媒体类型
        const HTML5MediaTypes = {
            '.mp4': true,
            '.mp3': true,
            '.wav': true,
            '.ogg': true
        };
        const mediaWrapper = document.getElementById('mediaWrapper');
        const displayedHeightVal = 150;

        // 加载媒体文件
        const loadMedia = (url, type) => new Promise((resolve, reject) => {
            var mediaObj = document.createElement(type);
            mediaObj.addEventListener('canplay', () => resolve(mediaObj));
            mediaObj.addEventListener('error', (err) => reject(err));
            mediaObj.src = url;
        });

        // 渲染处理后的输出
        async function renderProcessedOutput(encodedData, mediaType, outputFileExt) {
            if (typeof HTML5MediaTypes[outputFileExt.toLowerCase()] !== 'undefined') {
                try {
                    let loadedMediaObj = await loadMedia(encodedData, mediaType);
                    loadedMediaObj.setAttribute('controls', '');
                    await new Promise((resolve, reject) => setTimeout(resolve, 50));

                    if (mediaType == 'video') {
                        let mediaObjHeight = loadedMediaObj.videoHeight;
                        let mediaObjWidth = loadedMediaObj.videoWidth;

                        let scaleRatio = parseFloat(displayedHeightVal / mediaObjHeight);
                        let displayedHeight = scaleRatio * mediaObjHeight;
                        let displayedWidth = scaleRatio * mediaObjWidth;
                        loadedMediaObj['style']['height'] = `${displayedHeight}px`;
                        loadedMediaObj['style']['width'] = `${displayedWidth}px`;
                        loadedMediaObj['style']['margin'] = '0 auto';
                        await new Promise((resolve, reject) => setTimeout(resolve, 50));
                    }
                    mediaWrapper.appendChild(loadedMediaObj);
                } catch (errMsg) {
                    console.error(errMsg);
                }
            } else {
                let fillerDIV = document.createElement('div');
                fillerDIV.className = 'border';
                fillerDIV['style']['height'] = `${displayedHeightVal}px`;
                fillerDIV['style']['width'] = `${displayedHeightVal}px`;
                fillerDIV['style']['margin'] = '0 auto';

                fillerDIV.innerHTML = 'Content is not HTML5 compatible for display.';
                mediaWrapper.appendChild(fillerDIV);
            }
            return Promise.resolve('Conversion Success!');
        }

上传文件并转换函数(关键运算)

        // 上传文件改变事件
        uploadMedia.addEventListener('change', async (evt) => {
            outputFileExtension.disabled = true;
            uploadMediaBtn.disabled = true;

            const outputFileMimeType = MimeTypeDisplay.innerHTML;
            const outputFileExt = FileExtDisplay.innerHTML;

            const file = evt.target.files[0];
            if (!file) return;
            let fileName = file.name;
            let fileType = file.type;
            let fileSizeInKB = parseInt(file.size / 1024);
            let fileSizeInMB = ((file.size / 1024) / 1024).toFixed(2);

            fileNameDisplay.innerHTML = fileName;
            fileTypeDisplay.innerHTML = fileType;
            fileSizeDisplay.innerHTML = `${fileSizeInKB} <strong class="symbol">𝚔𝙱</strong> <span class="symbol">≈</span> ${fileSizeInMB} <strong class="symbol">𝙼𝙱</strong>`;
			// 使用指定路径创建 FFmpeg 实例
            appendDataLog('Initialising FFmpeg.');
            const ffmpeg = FFmpeg.createFFmpeg({
                corePath: new URL('js/ffmpeg/ffmpeg-core.js', document.location).href,
                workerPath: new URL('js/ffmpeg/ffmpeg-core.worker.js', document.location).href,
                wasmPath: new URL('js/ffmpeg/ffmpeg-core.wasm', document.location).href,
                log: true
            });
            await ffmpeg.load();
            appendDataLog('FFmpeg has loaded.');
			// 将文件读取为数组缓冲区,然后将数组缓冲区转换为 Uint8Array
            appendDataLog('Reading input file.');
            let arrBuffer = await readFileAsArrayBuffer(file);
            let uInt8Array = new Uint8Array(arrBuffer);

            appendDataLog('Writing to input file.');
            ffmpeg.FS('writeFile', fileName, uInt8Array);// https://emscripten.org/docs/api_reference/Filesystem-API.html            // https://segmentfault.com/a/1190000039308144            // ffmpeg.FS("writeFile",  "input.avi",  new Uint8Array(sourceBuffer, 0, sourceBuffer.byteLength));

            appendDataLog('Transcoding input file to output file.');
            await ffmpeg.run('-i', fileName, `output${outputFileExt}`);

            appendDataLog('Retrieving output file from virtual files system.');
            const data = ffmpeg.FS('readFile', `output${outputFileExt}`); // Uint8Array 

            let b64Str = convertBitArrtoB64(data);
            let encodedData = `data:${outputFileMimeType};base64,${b64Str}`;
            appendDataLog('File conversion has been successfully completed.');

            saveOutputBtn.disabled = false;
            saveOutputBtn.value = encodedData;

            let mediaType = 'audio';
            if (!outputFileMimeType.includes(mediaType)) {
                mediaType = 'video';
            }
            let status = await renderProcessedOutput(encodedData, mediaType, outputFileExt);
            appendDataLog(status);

            ffmpeg.FS('unlink', `output${outputFileExt}`);
            await new Promise((resolve, reject) => setTimeout(resolve, 50));
            ffmpeg.exit();
        });

保存输出

        // 保存输出按钮点击事件
        saveOutputBtn.addEventListener('click', async () => {
            let dwnlnk = document.createElement('a');

            let fileName = fileNameDisplay.innerHTML;
            let outputFileExt = FileExtDisplay.innerHTML;

            let saveFilename = fileName.substr(0, fileName.lastIndexOf('.'));
            dwnlnk.download = `${saveFilename}${outputFileExt}`;
            dwnlnk.href = saveOutputBtn.value;
            dwnlnk.click();
        });

重置所有按钮点击事件

        // 重置所有按钮点击事件
        function resetAll() {
            if (mediaWrapper.children.length > 0) {
                mediaWrapper.removeChild(mediaWrapper.children[0]);
            }
            outputFileExtension.disabled = false;
            outputFileExtension.selectedIndex = 0;
            triggerEvent(outputFileExtension, 'change');

            uploadMediaBtn.disabled = false;
            uploadMedia.value = '';

            fileNameDisplay.innerHTML = '<span class="symbol">…</span>';
            fileTypeDisplay.innerHTML = '<span class="symbol">…</span>';
            fileSizeDisplay.innerHTML = '<span class="symbol">…</span>';

            outputLogs.innerHTML = '';

            saveOutputBtn.value = '';
            saveOutputBtn.disabled = true;
        }

        // 重置所有按钮点击事件绑定
        resetAllBtn.addEventListener('click', async () => {
            resetAll();
        });

    });
}

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

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

相关文章

政安晨:示例演绎Python的列表

列表和你可以用它们做的事&#xff1a;包括索引、切片和对象变动 (变异-Mutation) 。 列表 在Python中&#xff0c;列表表示有序的值序列。以下是如何创建列表的示例&#xff1a; primes [2, 3, 5, 7] 我们可以将其他类型的元素放在列表中&#xff1a; planets [Mercury…

预充电阻原理作用,直流预充电电路设计指南

最初将电池连接到具有容性输入的负载时&#xff0c;当负载电容充电至电池电压时&#xff0c;会出现电流涌入。 对于大型电池&#xff08;具有低源电阻&#xff09;和强大的负载&#xff08;输入端具有大电容器&#xff09;&#xff0c;浪涌电流可以很容易地达到 1000 A 的峰值。…

WPS Office18.7软件日常更新

【应用名称】&#xff1a;WPS Office 【适用平台】&#xff1a;#Android 【软件标签】&#xff1a;#WPS 【应用版本】&#xff1a;18.6.1➡18.7 【应用大小】&#xff1a;160MB 【软件说明】&#xff1a;软件日常更新。WPS Office是使用人数最多的移动办公软件。独有手机阅读模…

C++棋类小游戏2

今天给大家带来我花了1周时间自创的小游戏的升级版&#xff0c;博主还是一名小学生&#xff0c;希望大家提提意见。这是我写的最长的C代码&#xff0c;希望大家喜欢&#xff0c;不要抄袭&#xff0c;任何编译器都可以。 以前版本——C自创棋类小游戏-CSDN博客 C内容提示&…

python 爬虫安装http请求库

我的是window环境&#xff0c;安装的python3&#xff0c;如果再linux环境&#xff1a;pip install requests 开始&#xff1a; 上面我们成功发送请求并获取到响应&#xff0c;现在需要解析html或xml获取数据&#xff0c;因此我使用现成的工具库Beautiful Soup

【用Unity开发一款横板跳跃游戏部分需要学习的技术点指南】

*** 用Unity开发一款横板跳跃游戏部分需要学习的技术点指南 空洞骑士是一款基于横板平台跳跃的传统风格2D动作冒险游戏&#xff0c;庞大的游戏世界交错相通&#xff0c;玩家控制小虫子去探索幽深黑暗的洞穴&#xff0c;成为了一代人茶余饭后的惦念&#xff0c;深受玩家喜爱。 …

AI新宠Arc浏览器真可以取代Chrome吗?

每周跟踪AI热点新闻动向和震撼发展 想要探索生成式人工智能的前沿进展吗&#xff1f;订阅我们的简报&#xff0c;深入解析最新的技术突破、实际应用案例和未来的趋势。与全球数同行一同&#xff0c;从行业内部的深度分析和实用指南中受益。不要错过这个机会&#xff0c;成为AI领…

整理:汉诺塔简析

大体上&#xff0c;要解决一个汉诺塔问题&#xff0c;就需要解决两个更简单的汉诺塔问题 以盘子数量 3 的汉诺塔问题为例 要将 3 个盘子从 A 移动到 C&#xff0c;就要&#xff1a; 将两个盘子从 A 移动到 B&#xff08;子问题 1&#xff09; 为了解决子问题 1&#xff0c;就…

Leetcode—2881. 创建新列【简单】

2024每日刷题&#xff08;一零九&#xff09; Leetcode—2881. 创建新列 实现代码 import pandas as pddef createBonusColumn(employees: pd.DataFrame) -> pd.DataFrame:employees[bonus] employees[salary] * 2return employees 运行结果 之后我会持续更新&#xf…

给mysql设置时区

每次重启MySQL服务器后&#xff0c;使用IDEA的database navigator连接都会出现这种情况 解决方式就是 命令行登录后 set global time_zone 8:00;嘿嘿把之前自家简书文章 给mysql设置时区 搬运过来了&#xff0c;方便查阅

[基础IO]文件描述符{重定向/perror/磁盘结构/inode/软硬链接}

文章目录 1. 再识重定向2.浅谈perror()3.初始文件系统4.软硬链接 1. 再识重定向 图解./sf > file.txt 2>&1 1中内容拷贝给2 使得2指向file 再学一个 把file的内容传给cat cat拿到后再给file2 2.浅谈perror() open()接口调用失败返回-1,并且错误码errno被适当的设置,…

详解SkyWalking前端监控的性能指标

SkyWalking 从8.2.0版本开始支持对前端浏览器端的性能进行监控&#xff0c;不仅可以像以前一样监控浏览器发送给后端服务的与请求&#xff0c;还能看到前端的渲染速度、错误日志等信息——这些信息是获取最终用户体验的最有效指标。实现的方式是引入skywalking-client-js库&…

二叉树可视化

二叉树可视化 运行演示代码和程序已上传二叉树知识平衡二叉树红黑树最优二叉搜索树哈夫曼树KD树B树和B树 参考 运行演示 学习二叉树总是脑补图像&#xff0c;实在是恶心&#xff0c;就想写一个能可视化的二叉树&#xff0c;结果没控制好&#xff0c;功能越想越多&#xff0c;先…

【Linux】Linux 开发工具(vim、gcc/g++、make/Makefile)+【小程序:进度条】-- 详解

我们在 Windows 中编写 C/C 程序时&#xff0c;常用的 VS2019 是一个集成开发环境&#xff0c;包含了很多工具包。而在 Linux 下开发&#xff0c;大部分的情况下都是使用一个个独立的工具。比如&#xff1a;编写代码用 vim&#xff0c;编译代码用 gcc&#xff0c;调试代码用 gd…

异步编程Completablefuture使用详解----进阶篇

JDK版本&#xff1a;jdk17 IDEA版本&#xff1a;IntelliJ IDEA 2022.1.3 文章目录 前言一、异步任务的交互1.1 applyToEither1.2 acceptEither1.3 runAfterEither 二、get() 和 join() 区别三、ParallelStream VS CompletableFuture3.1 使用串行流执行并统计总耗时3.2 使用并行…

《幻兽帕鲁》开荒最强帕鲁推荐!轻松拿下各种BOSS 幻兽帕鲁爆火 幻兽帕鲁2月服务器费用7000万 幻兽帕鲁图鉴

最近一款叫做《幻兽帕鲁》的新游戏走红&#xff0c;成为了Steam游戏平台上&#xff0c;连续3周的销量冠军&#xff0c;有不少Mac电脑用户&#xff0c;利用CrossOver成功玩上了《幻兽帕鲁》&#xff0c;其实CrossOver已经支持很多3A游戏&#xff0c;包括《赛博朋克2077》《博德之…

Ps:自动对齐图层

Ps菜单&#xff1a;编辑/自动对齐图层 Edit/Auto-Align Layers 自动对齐图层 Auto-Align Layers命令通过分析选中图层上的图像&#xff0c;识别出图像间的共同特征点&#xff08;如边缘、纹理或特定标记等&#xff09;&#xff0c;然后基于这些特征点变换&#xff08;移动、旋转…

python 爬虫篇(2)---->re正则实战豆瓣读书爬取(附带源码)

re正则实战—豆瓣读书爬取 提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档 文章目录 re正则实战---豆瓣读书爬取前言一、准备工具二、构建请求头三、请求数据四、解析数据五、保存数据总结(源码)前言 大家好,今天我们来写一个豆瓣读书的爬虫程序,我会只用…

ProcessSlot构建流程分析

ProcessorSlot ProcessorSlot构建流程 // com.alibaba.csp.sentinel.CtSph#lookProcessChain private Entry entryWithPriority(ResourceWrapper resourceWrapper, int count, boolean prioritized, Object... args)throws BlockException {// 省略创建 Context 的代码// 黑盒…

Rust 第一个rust程序Hello Rust️

文章目录 前言一、vscode 安装rust相关插件二、Cargo New三、vscode调试rustLLDB 前言 Rust学习系列。今天就让我们掌握第一个rust程序。Hello Rust &#x1f980;️。 在上一篇文章我们在macOS成功安装了rust。 一、vscode 安装rust相关插件 以下是一些常用的 Rust 开发插件…