WebAssembly002 FFmpegWasmLocalServer项目

news2024/11/15 9:13:48

项目介绍

  • 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();
        });

    });
}

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

2、安全开发-Python-Socket编程端口探针域名爆破反弹Shell编码免杀

用途&#xff1a;个人学习笔记&#xff0c;欢迎指正&#xff01; 目录 主要内容&#xff1a; 一、端口扫描(未开防火墙情况) 1、Python关键代码: 2、完整代码&#xff1a;多线程配合Queue进行全端口扫描 二、子域名扫描 三、客户端&#xff0c;服务端Socket编程通信cmd命…

Python GCN、GAT、MP等图神经网络学习,从入门全面概述和讲解GNN,入门到精通图神经网络

1. 图的分类&#xff1a; 1.1 根据边的方向性&#xff1a; 有向图&#xff08;Directed Graph&#xff09;&#xff1a;图中的边具有方向性&#xff0c;表示节点之间的单向关系。例如&#xff0c;A指向B的边表示节点A指向节点B。无向图&#xff08;Undirected Graph&a…

LeetCode热题HOT100【栈的压入、弹出序列】

&#x1f525;LeetCode热题HOT100【栈的压入、弹出序列】 1. 题目来源2.题目 1. 题目来源 来自LeetCode热题HOT100 https://leetcode.cn/studyplan/top-100-liked/?isDarktrue 2.题目 题目地址 Leetcode地址 3.Stack 在Java中&#xff0c;Stack 是一个基于后进先出&#…

SpringCloud服务通信

目录 Ribbon实现服务通信 创建工程product-basic-provider&#xff08;提供者&#xff09; 创建工程product-img-provider&#xff08;提供者&#xff09; 创建工程product-detail-api&#xff08;消费者&#xff09; OpenFeign实现服务通信 创建工程product-detail-api-…

关于Ubuntu下docker-mysql:ERROR 2002报错

报错场景&#xff1a; mysql容器创建好后登录mysql时即使密码正确也是报出下方提示&#xff1a; 原因是在创建mysql容器在创建时本地目录缺失&#xff0c; 先去自建一个目录&#xff0c;例如&#xff1a; /opt/my_sql 正确完整目录如下&#xff1a; docker run --namemys…

EasyCVR视频融合平台如何助力执法记录仪高效使用

旭帆科技的EasyCVR平台可接入的设备除了常见的智能分析网关与摄像头以外 &#xff0c;还可通过GB28181协议接入执法记录仪&#xff0c;实现对执法过程的全称监控与录像&#xff0c;并对执法轨迹与路径进行调阅回看。那么&#xff0c;如何做到执法记录仪高效使用呢&#xff1f; …

20240202在Ubuntu20.04.6下配置环境变量之后让nvcc --version显示正常

20240202在Ubuntu20.04.6下配置环境变量之后让nvcc --version显示正常 2024/2/2 20:19 在Ubuntu20.04.6下编译whiper.cpp的显卡模式的时候&#xff0c;报告nvcc异常了&#xff01; 百度&#xff1a;nvcc -v nvidia-cuda-toolkit rootrootrootroot-X99-Turbo:~/whisper.cpp$ WH…

【Python基础】matplotlib 使用指南

文章目录 matplotlib 使用指南1 matplotlib安装与基本介绍1.1 %matplotlib inline1.2 认识matplotlib图表 2 matplotlib基本使用2.1 参数2.2格式配置2.3设置x与y轴2.4 字体设置2.5 title/图例设置2.6 子图2.7 创建画布2.8 设置坐标轴 3 matplotlib常用的图表3.1 折线图3.1 散点…

vue3、vue2以及非vue项目中拖拽改变dom结构以及数组顺序 vuedraggable

文章目录 vue3、vue2以及非vue项目中拖拽改变dom结构以及数组顺序先看效果属性名称 说明具体代码 vue3、vue2以及非vue项目中拖拽改变dom结构以及数组顺序 参考链接&#xff0c;这个还有其他库的教程&#xff0c;可以收藏的 展示效果 github地址 vue.draggable.next 是一款vu…

如何使用 ZoomEye 搜索引擎保姆级教程(附链接)

一、介绍 ZoomEye&#xff08;中文名&#xff1a;钟馗之眼&#xff09;是一款专注于网络设备和物联网设备搜索的搜索引擎。它提供了一种通过互联网上的设备进行搜索的方式&#xff0c;使用户能够发现和分析各种连接到互联网的设备&#xff0c;包括服务器、路由器、摄像头、数据…

Linux 网络编程 + 笔记

协议&#xff1a;一组规则 分层模型结构&#xff1a; OSI七层模型&#xff1a;物理层、数据链路层、网络层、传输层、会话层、表示层、应用层TCP/IP 4层模型&#xff1a;链路层/网络接口层、网络层、传输层、应用层 应用层&#xff1a;http、ftp、nfs、ssh、telnet、传输层&am…

【24美赛思路已出】2024年美赛A~F题解题思路已出 | 无偿自提

A题&#xff1a;资源可用性和性别比例 问题一&#xff1a; 涉及当灯鱼种群的性别比例发生变化时&#xff0c;对更大的生态系统产生的影响。为了分析这个问题&#xff0c;可以采用以下的数学建模思路&#xff1a;建立灯鱼种群模型&#xff1a; 首先&#xff0c;建立一个灯鱼种群…

爬虫入门到精通_基础篇4(BeautifulSoup库_解析库,基本使用,标签选择器,标准选择器,CSS选择器)

1 Beautiful说明 BeautifulSoup库是灵活又方便的网页解析库&#xff0c;处理高效&#xff0c;支持多种解析器。利用它不用编写正则表达式即可方便地实线网页信息的提取。 安装 pip3 install beautifulsoup4解析库 解析器使用方法优势劣势Python标准库BeautifulSoup(markup,…

Session与Cookie、部署redis、redis基本操作、Session共享

1 案例1&#xff1a;PHP的本地Session信息 1.1 问题 通过Nginx调度器负载后端两台Web服务器&#xff0c;实现以下目标&#xff1a; 部署Nginx为前台调度服务器调度算法设置为轮询后端为两台LNMP服务器部署测试页面&#xff0c;查看PHP本地的Session信息 1.2 方案 实验拓扑…

亚马逊新店铺视频怎么上传?视频验证失败怎么办?——站斧浏览器

亚马逊新店铺视频怎么上传&#xff1f; 登录亚马逊卖家中心&#xff1a;首先&#xff0c;卖家需要登录亚马逊卖家中心。在登录后&#xff0c;可以点击左侧导航栏上的“库存”选项&#xff0c;然后选择“新增或管理商品”。 选择商品&#xff1a;接下来&#xff0c;在“新增或…

如何部署Node.js服务并实现无公网ip远程访问本地项目【内网穿透】

文章目录 前言1.安装Node.js环境2.创建node.js服务3. 访问node.js 服务4.内网穿透4.1 安装配置cpolar内网穿透4.2 创建隧道映射本地端口 5.固定公网地址 前言 Node.js 是能够在服务器端运行 JavaScript 的开放源代码、跨平台运行环境。Node.js 由 OpenJS Foundation&#xff0…

YOLOv8进阶 | 如何用yolov8训练自己的数据集(以安全帽佩戴检测举例)

前言&#xff1a;Hello大家好&#xff0c;我是小哥谈。YOLOv8是一种目标检测算法&#xff0c;它是YOLO&#xff08;You Only Look Once&#xff09;系列算法的最新版本。本节课就带领大家如何基于YOLOv8来训练自己的目标检测模型&#xff0c;本次作者就以安全帽佩戴检测为案例进…

二叉树的最小深度

给定一个二叉树&#xff0c;找出其最小深度。 最小深度是从根节点到最近叶子节点的最短路径上的节点数量。 说明&#xff1a;叶子节点是指没有子节点的节点。 示例 1&#xff1a; 输入&#xff1a;root [3,9,20,null,null,15,7] 输出&#xff1a;2示例 2&#xff1a; 输入&…

postgres:锁申请

什么是弱锁&#xff0c;强锁&#xff1f; 为了提高并发控制&#xff0c;PG通过将锁信息在本地缓存&#xff08;**LOCALLOCK**&#xff09;和快速处理常见锁&#xff08;fastpath&#xff09;&#xff0c;减少了对共享内存的访问&#xff0c;提高性能。从而出现了弱锁和强锁的概…

如何在win系统部署开源云图床Qchan并无公网ip访问本地存储图片

文章目录 前言1. Qchan网站搭建1.1 Qchan下载和安装1.2 Qchan网页测试1.3 cpolar的安装和注册 2. 本地网页发布2.1 Cpolar云端设置2.2 Cpolar本地设置 3. 公网访问测试总结 前言 图床作为云存储的一项重要应用场景&#xff0c;在大量开发人员的努力下&#xff0c;已经开发出大…