WEB技术调研:【视频切片+播放】
源码地址
此次调研的源代码已放在GitHub,欢迎访问:
https://github.com/wenkil/video-m3u8-demo
调研背景
SCORM视频包通常会将一些图片、视频资源直接放到包内提供,从公司产品角度需要考虑内容的传播和使用,比如直接打开包内找到视频的mp4文件,自己存下来或转播给其他人的操作是不被允许的,这时候需要借助一些手段来做一层防护,防止用户直接拿走mp4格式的视频资源。
本文主要借助ffmpeg视频分割技术来展示如何将一个MP4视频分割为M3U8列表格式的视频切片,并在前端项目里加载这些分割后的切片进行播放;
(以下内容来自智谱清言)
什么是FFmpeg?
FFmpeg是一个开源项目,它包含了一套完整的跨平台的音视频处理工具和库,用于处理音频和视频文件。FFmpeg是由FFmpeg社区维护的,它支持大量的音视频格式,并且可以在多种操作系统上运行,包括Windows、macOS和Linux等。
FFmpeg的主要组件包括:
FFmpeg命令行工具:这是FFmpeg中最常用的部分,它提供了一个命令行界面,允许用户执行各种音视频处理任务,如转换格式、压缩、解码、编码、剪辑、过滤和播放等。
libavcodec:这是FFmpeg的核心库之一,提供了音视频编解码功能。它包含了大量的编解码器,可以处理几乎所有的音视频格式。
libavformat:这个库负责处理各种音视频封装格式,如MP4、MKV、OGG等。它提供了读取和写入这些格式的能力。
libavutil:这是一个包含了一些通用工具和实用程序的库,如数学计算、数据结构、随机数生成器等。
libavfilter:这个库提供了一系列的音视频过滤器,可以用来处理音视频流,如应用效果、进行转换等。
libavresample:这个库用于音视频采样率转换和通道布局转换。
FFmpeg的使用非常广泛,它被用于各种场合,如视频剪辑、视频转换、音频处理、流媒体传输等。FFmpeg的命令行工具是通过执行一系列的FFmpeg库来完成的,它提供了一个简洁的命令行界面,允许用户通过输入命令来执行各种操作。
FFmpeg的安装通常涉及到下载源代码或预编译的二进制文件,然后将其安装到系统中。在安装后,你可以在命令行中使用FFmpeg的命令来执行各种音视频处理任务。
例如,以下是一些常用的FFmpeg命令:
ffmpeg -i input.mp4 output.avi:将输入文件input.mp4转换为output.avi。
ffmpeg -i input.mp3 output.wav:将输入文件input.mp3转换为output.wav。
ffmpeg -i input.mp4 -ss 00:00:10 -to 00:00:20 -c copy output.mp4:从input.mp4中剪辑出从第10秒到第20秒的片段,并保存为output.mp4。
FFmpeg是一个功能强大的工具,对于音视频处理来说是一个非常有用的资源。由于其开源和跨平台的特性,它被广泛应用于个人和商业项目中。
什么是M3U8?
M3U8是一种用于多媒体播放列表的格式,它广泛用于网络流媒体中,尤其是用于视频流和音频流。M3U8播放列表可以包含多个媒体文件的URL,这些文件按顺序排列,以便于播放器能够依次播放它们。M3U8格式支持视频流媒体服务中的无缝播放,即视频在缓冲期间可以播放下一段的预加载内容,从而给用户带来平滑的观看体验。
M3U8播放列表的文件扩展名通常是.m3u8。这种格式的播放列表可以包含以下信息:
媒体文件的URL:播放列表中包含每个媒体文件的URL,这些URL指向存储媒体文件的服务器地址。
媒体文件的播放顺序:播放列表中文件的排列顺序指示了媒体文件应按照该顺序播放。
媒体文件的持续时间:有时播放列表中还包含媒体的持续时间信息,以便于播放器进行时间同步和播放进度控制。
M3U8格式有两种主要的变体:
基本M3U8:这种变体不包含关于媒体文件片段的任何信息,只是简单地列出了媒体文件的URL。基本M3U8适用于不支持流媒体的旧版播放器。
扩展M3U8:这种变体包含了关于媒体文件片段的更多信息,如片段的持续时间和byterange,这有助于播放器更有效地处理和播放视频流。扩展M3U8通常用于现代流媒体服务中。
在视频流媒体服务中,视频文件通常会被分割成多个较小的片段,每个片段是一个独立的媒体文件。M3U8播放列表包含了这些片段的URL,并且按照播放顺序排列。当用户请求播放视频时,流媒体服务器会根据M3U8播放列表依次传输这些片段,并在播放器中按顺序播放它们。由于M3U8支持无缝播放,视频流在缓冲期间可以播放下一段的预加载内容,从而实现平滑的观看体验,无需等待整个视频下载完成。
M3U8格式在直播视频和VoD(视频点播)服务中得到了广泛应用,因为它能够适应不同的网络条件和设备性能,提供稳定可靠的流媒体服务。
视频切片代码
首先创建一个node项目,提供一个api,可以将提供的视频进行分割,主要借助FFmpeg 的npm库,执行FFmpeg 命令进行分割,代码如下:
const express = require('express');
const router = express.Router();
const multer = require('multer');
const ffmpeg = require('ffmpeg-static');
const { exec } = require('child_process');
const path = require('path');
const fs = require('fs');
// 设置上传目录和文件名
const storage = multer.diskStorage({
destination: function (req, file, cb) {
const uploadDir = path.join(__dirname, '../uploads/');
// 确保上传目录存在
if (!fs.existsSync(uploadDir)){
fs.mkdirSync(uploadDir, { recursive: true });
}
cb(null, uploadDir);
},
filename: function (req, file, cb) {
cb(null, file.originalname);
}
});
const upload = multer({ storage: storage });
// 定义上传和转换视频的路由
router.post('/upload', upload.single('video'), (req, res) => {
const videoPath = req.file.path;
const outputDir = path.join(__dirname, '../uploads/', path.parse(videoPath).name);
if (!fs.existsSync(outputDir)){
fs.mkdirSync(outputDir, { recursive: true });
}
const outputPath = path.join(outputDir, 'video.m3u8');
// 使用FFmpeg转化视频
const ffmpegCommand = ${ffmpeg} -i "${videoPath}" -codec: copy -start_number 0 -hls_time 10 -hls_list_size 0 -f hls "${outputPath}";
exec(ffmpegCommand, (error, stdout, stderr) => {
if (error) {
console.error(exec error: ${error});
return res.status(500).send({ message: '视频转换失败:'+error });
}
console.log(stdout: ${stdout});
console.error(stderr: ${stderr});
res.send({ message: '视频转换完成,转换目录:', path: outputPath });
});
});
router.get('/convert-videos', (req, res) => {
const videosDir = path.join(__dirname, '../videos'); // 视频文件所在目录
fs.readdir(videosDir, (err, files) => {
if (err) {
console.error(Error reading directory: ${err});
return res.status(500).send({ message: '读取文件夹失败!!!' });
}
// 过滤出视频文件,这里简单以.mp4为例
const videoFiles = files.filter(file => file.endsWith('.mp4'));
if (videoFiles.length === 0) {
return res.send({ message: '没有找到mp4文件!!!' });
}
videoFiles.forEach(videoName => {
const videoPath = path.join(videosDir, videoName);
const outputDir = path.join(videosDir, 'output', path.parse(videoName).name);
if (!fs.existsSync(outputDir)) {
fs.mkdirSync(outputDir, { recursive: true });
}
const outputPath = path.join(outputDir, 'video.m3u8');
// 使用FFmpeg转化视频
const ffmpegCommand = ${ffmpeg} -i "${videoPath}" -codec: copy -start_number 0 -hls_time 10 -hls_list_size 0 -f hls "${outputPath}";
exec(ffmpegCommand, (error, stdout, stderr) => {
if (error) {
console.error(Error converting ${videoName}: ${error});
return; // 在这里,我们简单地返回,你可能想要处理错误或记录到错误日志
}
console.log(${videoName} conversion succeeded.);
});
});
res.send({ message: 'videos文件夹下的所有视频转换完成!~~' });
});
});
module.exports = router;
这里的js里提供了两种方式,一种上传的方式,一种是直接遍历指定文件夹下的所有视频而不需要上传的方式;
新建一个upload.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Upload Video</title>
</head>
<body>
<h1>Upload a Video</h1>
<form action="/upload" method="post" enctype="multipart/form-data">
<input type="file" name="video" accept="video/*" required>
<button type="submit">Upload Video</button>
</form>
</body>
</html>
启动node项目,在浏览器打开:http://localhost:xxxx/upload.html , 展示如下:
点击选择文件,上传视频,然后点击Upload Video按钮,这时候会自动执行router.post(‘/upload’) 的方法,主要逻辑为读取视频文件名称,并通过ffmpeg命令执行分割,该命令会自动把分割后的文件输出到指定目录,即uploads文件夹下,页面展示如图:
在文件夹的目录展示如下:会有一个xxx.m3u8和n个ts格式的文件:
注:代码中/convert-videos 的方法逻辑相同,会将指定文件夹下的所有视频自动转换完成,并按照视频文件名进行单独的目录保存;
前端播放m3u8格式的视频
接下来拿到这些切片的视频,在前端项目里进行保存;
前端播放主要使用了hls.js这个库,GitHub地址:https://github.com/video-dev/hls.js
以下是使用方式,及播放效果:
videoList.vue:
<template>
<div class="flex-column-list">
<button class="video-btn" v-for="(video, index) in videos" :key="index" @click="selectVideo(video)">
{{ video.name }}
</button>
</div>
</template>
<script setup>
const videos = [
{name: 'Video 1', url: '/video1/video.m3u8'},
{name: 'Video 2', url: '/video2/video.m3u8'},
{name: 'Video 3', url: '/video3/video.m3u8'},
{name: 'Video 4', url: '/video4/video.m3u8'},
{name: 'Video 5', url: '/video5/video.m3u8'},
];
const emit = defineEmits(['update:videoUrl']);
const selectVideo = (video) => {
emit('update:videoUrl', video.url);
};
</script>
<style scoped>
.flex-column-list {
display: flex;
flex-direction: column;
flex-wrap: nowrap;
}
.video-btn {
margin: 5px;
}
</style>
videoPlayer.vue:
<template>
<video ref="videoElement" controls style="width: 100%; max-width: 640px;"></video>
</template>
<script setup>
import { ref, watchEffect } from 'vue';
import Hls from 'hls.js';
// Props to receive the selected video URL
const props = defineProps({
videoUrl: String,
});
const videoElement = ref(null);
watchEffect(() => {
if (!props.videoUrl) return;
if (Hls.isSupported()) {
console.log('HLS supported');
const hls = new Hls();
hls.loadSource(props.videoUrl);
hls.attachMedia(videoElement.value);
hls.on(Hls.Events.MANIFEST_PARSED, function() {
// videoElement.value.play();
});
} else if (videoElement.value.canPlayType('application/vnd.apple.mpegurl')) {
console.log('不支持HLS');
videoElement.value.src = props.videoUrl;
videoElement.value.addEventListener('loadedmetadata', () => {
// videoElement.value.play();
});
}
});
</script>
点击对应的video按钮,会加载对应的视频指向的地址,点击视频的播放按钮即可正常播放切片的视频,效果如图:
本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!