vue3项目实战 仿百度网盘
hls的github文档中的api详细说明
hlsjs-dev官方的demo演示
hls播放m3u8 添加header请求头,在请求ts的url上添加参数
Vue 之 视频流 - Hls.js
hls.js如何播放m3u8文件(实例)
网页前端video播放m3u8(HLS)
ffmpeg 视频ts切片生成m3u8
mp4视频分片生成m3u8流文件并加密,
Java使用FFmpeg(自定义cmd)系列之官方API获取视频/音频信息(URL方式)
Java使用FFmpeg(自定义cmd)系列之MP4 转码 HLS m3u8 AES128 加密(源码已保存在gitee上)
这个加密后面可以试一下,因为分片完,前端随便拿一个分片,这个分片单独保存下来使用vlc播放器也是可以播放的
Spring boot视频播放(解决MP4大文件无法播放),整合ffmpeg,用m3u8切片播放。(包含加密方式)
Java使用ffmpeg进行视频格式转换、音视频合并、播放、截图
DPlayer官网文档
vue使用dplayer 播放m3u8格式的视频——播放m3u8格式视频(三)
vue使用vue-dplayer播放m3u8格式的视频——播放m3u8格式视频(二)
vue使用原生videojs 播放m3u8格式的视频——播放m3u8格式视频(一)
MSE(Media Source Extensions)介绍
HLS (HTTP Live Streaming)是Apple公司研发的流媒体传输技术,包括一个m3u8的索引文件、多个ts分片文件和key加密串文件。这项技术主要应用于点播和直播领域。
npm install --save hls.js
<template>
<div style="text-align: center;">
<video id="video" ref="videoRef" width="800" height="400" controls>video>
<p>
<button @click="playVideo">播放视频button>
<button @click="pauseVideo">暂停视频button>
p>
div>
template>
<script setup>
import { ref, reactive, onMounted } from 'vue'
import Hls from 'hls.js';
const videoRef = ref()
var videoSrc = 'http://127.0.0.1:8085/ts/video/playM3U8video/YsIlFSjnlh'; // 用于获取m3u8文件的地址, 后端处理跨域
function playVideo() {
videoRef.value.play()
}
function pauseVideo() {
videoRef.value.pause()
}
function loadVideo() {
if (Hls.isSupported()) {
// 组件挂载完成, 才能获取到video元素
var video = document.getElementById('video');
// 可参考:
// hls的github文档中的api:(https://github.com/video-dev/hls.js/blob/master/docs/API.md)
// hls.js如何播放m3u8文件(实例)?(https://blog.csdn.net/ffffffff8/article/details/129314268)
// Vue 之 视频流 - Hls.js(https://blog.csdn.net/a15297701931/article/details/115478652)
let config = {
xhrSetup: function (xhr, url) {
xhr.withCredentials = true; // 会携带cookie
xhr.setRequestHeader('token',"my-token")
},
}
var hls = new Hls(config);
hls.on(Hls.Events.MEDIA_ATTACHED, function () {
console.log('video and hls.js are now bound together !');
});
hls.on(Hls.Events.MANIFEST_PARSED, function (event, data) {
console.log('manifest loaded, found ' + data.levels.length + ' quality level');
// video.play() // 不能在这里就播放, 需要用于与dom元素有交互才可以播放, 否则浏览器会报错
});
hls.loadSource(videoSrc);
// bind them together
hls.attachMedia(video);
}
}
onMounted(() => {
loadVideo()
})
script>
<style scoped>
p {
margin: 0;
}
style>
#EXTM3U
#EXT-X-VERSION:3
#EXT-X-MEDIA-SEQUENCE:0
#EXT-X-ALLOW-CACHE:YES
#EXT-X-TARGETDURATION:30
#EXTINF:30.000000,
YsIlFSjnlh_0000.ts
#EXTINF:30.000000,
YsIlFSjnlh_0001.ts
#EXTINF:30.000000,
YsIlFSjnlh_0002.ts
#EXTINF:30.000000,
YsIlFSjnlh_0003.ts
#EXTINF:30.000000,
YsIlFSjnlh_0004.ts
#EXTINF:30.000000,
YsIlFSjnlh_0005.ts
#EXTINF:30.000000,
YsIlFSjnlh_0006.ts
#EXTINF:30.000000,
YsIlFSjnlh_0007.ts
#EXTINF:30.000000,
YsIlFSjnlh_0008.ts
#EXTINF:30.000000,
YsIlFSjnlh_0009.ts
#EXTINF:30.000000,
YsIlFSjnlh_0010.ts
#EXTINF:30.000000,
YsIlFSjnlh_0011.ts
#EXTINF:30.000000,
YsIlFSjnlh_0012.ts
#EXTINF:30.000000,
YsIlFSjnlh_0013.ts
#EXTINF:30.000000,
YsIlFSjnlh_0014.ts
#EXTINF:30.000000,
YsIlFSjnlh_0015.ts
#EXTINF:30.000000,
YsIlFSjnlh_0016.ts
#EXTINF:30.000000,
YsIlFSjnlh_0017.ts
#EXTINF:5.400000,
YsIlFSjnlh_0018.ts
#EXT-X-ENDLIST
摘自 B站程序员老罗网盘项目代码
cutFile4Video
private void cutFile4Video(String fileId, String videoFilePath) {
// videoFilePath 如: D:/document/easypan/easypan-java/file//202305/{userId}{fileId}.mp4
//创建同名切片目录,
// tsFolder 与 视频文件在同一目录下,并且tsFolder是以视频真实文件名(不带后缀)作为文件夹名
// tsFolder 如: D:/document/easypan/easypan-java/file//202305/{userId}{fileId} - 这是个文件夹
File tsFolder = new File(videoFilePath.substring(0, videoFilePath.lastIndexOf(".")));
if (!tsFolder.exists()) {
tsFolder.mkdirs();
}
// 这里也尝试过直接对mp4文件直接转为.m3u8 + ts分段视频文件,但转的速度比较慢,不知道是不是参数设置的原因,ffmpeg不是很清楚它的命令
// 命令如:ffmpeg -i ./jvm.mp4 -c:v h264 -flags +cgop -g 30 -hls_time 60 -hls_list_size 0 -hls_segment_filename index%3d.ts index.m3u8
// 这个命令,会将执行命令时所在的当前目录下的jvm.mp4文件按照视频的每60s切割成一个ts文件 和 .m3u8索引文件 到 当前目录
// 这里先转为ts文件,然后,再切割这个ts文件,生成.m3u8索引文件(速度比上面快)
// 1. 将 整个视频文件 转成ts文件:index.ts
// ffmpeg -y -i {mp4视频文件路径} -vcodec copy -acodec copy -vbsf h264_mp4toannexb {放入到哪一个文件位置}
// 如:ffmpeg -y -i D:/test/jvm.mp4 -vcodec copy -acodec copy -vbsf h264_mp4toannexb D:/test/jvm/index.ts
// 这个命令会将 第一个 所指向的mp4视频文件 转成 ts文件 存储到 D:/test/jvm/index.ts,注意第二个文件路径在执行命令前必须要存在
// 2. 将index.ts文件进行分片
// ffmpeg -i {index文件的文件路径} -c copy -map 0 -f segment -segment_list {要生成的m3u8索引文件路径} -segment_time 30 {生成的ts切片路径}/{文件名前面部分}_%4d.ts
// 生成的ts文件路径%%4d,写了2个百分号是为了防止jdk的MessageFormat处理
// 如:ffmpeg -i D:/test/jvm/index.ts -c copy -map 0 -f segment -segment_list D:/test/jvm/index.m3u8 -segment_time 30 D:/test/jvm/jjvvmm_%4d.ts
// 这个 命令会将 第一个 所指向的ts视频文件 按照 每30s 切割成一个小的ts视频文件,放入到指定的文件夹中,并且有指定格式的文件名(占4位,递增),并且会生成一个m3u8的索引文件
// mp4转ts文件的ffmpeg命令
// 如:ffmpeg -y -i D:/document/easypan/easypan-java/file//202305/{userId}{fileId}.mp4 -vcodec copy -acodec copy -vbsf h264_mp4toannexb D:/document/easypan/easypan-java/file//202305/{userId}{fileId}/index.ts
final String CMD_TRANSFER_2TS = "ffmpeg -y -i %s -vcodec copy -acodec copy -vbsf h264_mp4toannexb %s";
// ts文件拆分成小的ts文件 和 生成一个.m3u8文件
// 如:ffmpeg -i D:/document/easypan/easypan-java/file//202305/{userId}{fileId}/index.ts -c copy -map 0 -f segment -segment_list D:/document/easypan/easypan-java/file//202305/{userId}{fileId}/index.m3u8 -segment_time 30 D:/document/easypan/easypan-java/file//202305/{userId}{fileId}/{fileId}_%%4d.ts
final String CMD_CUT_TS = "ffmpeg -i %s -c copy -map 0 -f segment -segment_list %s -segment_time 30 %s/%s_%%4d.ts";
// 转成的ts文件所在路径:D:/document/easypan/easypan-java/file//202305/{userId}{fileId}/index.ts
String tsPath = tsFolder + "/" + Constants.TS_NAME;
String cmd = String.format(CMD_TRANSFER_2TS, videoFilePath, tsPath);
ProcessUtils.executeCommand(cmd, false);
//生成索引文件.m3u8 和切片.ts
cmd = String.format(CMD_CUT_TS, tsPath, tsFolder.getPath() + "/" + Constants.M3U8_NAME, tsFolder.getPath(), fileId);
ProcessUtils.executeCommand(cmd, false);
//删除index.ts
new File(tsPath).delete();
}
ProcessUtils 工具类
package com.easypan.utils;
import com.easypan.exception.BusinessException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
public class ProcessUtils {
private static final Logger logger = LoggerFactory.getLogger(ProcessUtils.class);
public static String executeCommand(String cmd, Boolean outprintLog) throws BusinessException {
if (StringTools.isEmpty(cmd)) {
logger.error("--- 指令执行失败,因为要执行的FFmpeg指令为空! ---");
return null;
}
Runtime runtime = Runtime.getRuntime();
Process process = null;
try {
process = Runtime.getRuntime().exec(cmd);
// 执行ffmpeg指令
// 取出输出流和错误流的信息
// 注意:必须要取出ffmpeg在执行命令过程中产生的输出信息,如果不取的话当输出流信息填满jvm存储输出留信息的缓冲区时,线程就回阻塞住
PrintStream errorStream = new PrintStream(process.getErrorStream());
PrintStream inputStream = new PrintStream(process.getInputStream());
errorStream.start();
inputStream.start();
// 等待ffmpeg命令执行完
process.waitFor();
// 获取执行结果字符串
String result = errorStream.stringBuffer.append(inputStream.stringBuffer + "\n").toString();
// 输出执行的命令信息
if (outprintLog) {
logger.info("执行命令:{},已执行完毕,执行结果:{}", cmd, result);
} else {
logger.info("执行命令:{},已执行完毕", cmd);
}
return result;
} catch (Exception e) {
// logger.error("执行命令失败:{} ", e.getMessage());
e.printStackTrace();
throw new BusinessException("视频转换失败");
} finally {
if (null != process) {
ProcessKiller ffmpegKiller = new ProcessKiller(process);
runtime.addShutdownHook(ffmpegKiller);
}
}
}
/**
* 在程序退出前结束已有的FFmpeg进程
*/
private static class ProcessKiller extends Thread {
private Process process;
public ProcessKiller(Process process) {
this.process = process;
}
@Override
public void run() {
this.process.destroy();
}
}
/**
* 用于取出ffmpeg线程执行过程中产生的各种输出和错误流的信息
*/
static class PrintStream extends Thread {
InputStream inputStream = null;
BufferedReader bufferedReader = null;
StringBuffer stringBuffer = new StringBuffer();
public PrintStream(InputStream inputStream) {
this.inputStream = inputStream;
}
@Override
public void run() {
try {
if (null == inputStream) {
return;
}
bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
String line = null;
while ((line = bufferedReader.readLine()) != null) {
stringBuffer.append(line);
}
} catch (Exception e) {
logger.error("读取输入流出错了!错误信息:" + e.getMessage());
} finally {
try {
if (null != bufferedReader) {
bufferedReader.close();
}
if (null != inputStream) {
inputStream.close();
}
} catch (IOException e) {
logger.error("调用PrintStream读取输出流后,关闭流时出错!");
}
}
}
}
}
@RestController
@RequestMapping("ts/video")
public class TsController {
@Autowired
private TsService tsService;
@GetMapping("playM3U8video/{fileId}")
public void playM3U8video(@PathVariable("fileId") String fileId) {
tsService.playM3U8video(fileId);
}
}
@Slf4j
@Service
public class TsService {
private static final String VIDEO_ROOT_PATH = "d:\\test";
@Autowired
@SuppressWarnings("all")
private HttpServletResponse response;
// 可参考:ffmpeg 视频ts切片生成m3u8
// 网页前端video播放m3u8(HLS) https://www.cnblogs.com/fieldtianye/p/13166957.html
// https://www.cnblogs.com/fieldtianye/category/1789297.html
public void playM3U8video(String fileId) {
FileSystemResourceLoader fileResourceLoader = new FileSystemResourceLoader();
// 先获取m3u8文件
if (!fileId.endsWith("ts")) {
Resource resource = fileResourceLoader.getResource(VIDEO_ROOT_PATH + File.separator + fileId + "/index.m3u8");
try {
StreamUtils.copy(resource.getInputStream(), response.getOutputStream());
} catch (IOException e) {
log.error("写出m3u8文件流失败:{}",e);
}
} else {
// 请求以ts结尾,则获取ts分段文件
Resource resource = fileResourceLoader.getResource(VIDEO_ROOT_PATH + File.separator + fileId.split("_")[0] + File.separator +fileId);
try {
StreamUtils.copy(resource.getInputStream(), response.getOutputStream());
} catch (IOException e) {
log.error("写出m3u8文件流失败:{}",e);
}
}
}
}
效果与直接使用hls效果一致,但是优点是它还支持其它的MSE(Media Source Extensions)
携带了自定义的请求头
摘自:MSE(Media Source Extensions)介绍
本文介绍 MSE(Media Source Extensions) 的相关知识。
MSE(Media Source Extensions),即媒体源扩展,可以理解为一种API,其提供了实现无插件(js插件外的插件)且基于 Web 的流媒体的功能。通过 MSE,媒体串流能够通过 JavaScript 创建,并且可以使用 HTML5 的 和
标签进行播放。
以前,用户在通过Web浏览器浏览网页内容,尤其是视频内容时,需要使用诸如 Adobe Flash或是微软的 Silverlight 等类似的插件,才能播放视音频内容
,这些插件对于Web浏览器来说,扮演着媒体播放器角色。但是,在浏览器中使用插件是不便捷并且不安全的(不法分子会在插件上动手脚),因此,最新的HTML5标准中,定义了一系列新的元素来避免使用插件,其中就包含了大名鼎鼎的
。
有了
,但是 HTML5 的
我们可以把
从另外一个角度来说,通过引入 MSE,HTML5 的
。如此一来,我们就可以通过 (具备MSE功能的)JS,把一些 标签原本不支持的视频流格式,转化为其支持的格式(如 H.264 的 mp4)。B站开源的 flv.js 就是此技术的一个典型应用场景,B站的HTML5播放器,通过使用 MSE 技术,将FLV源用 JS(flv.js) 实时转码成 HTML5 支持的视频流编码格式,提供给 HTML5 播放器播放。
后端代码不变,修改前端代码即可。
npm i dplayer -S // 视频播放器插件
npm i hls.js -S // 播放hls流插件
<template>
<div id="dplayer" style="width: 400px;">div>
<p>
<button @click="playVideo">播放视频button>
<button @click="pauseVideo">暂停视频button>
p>
template>
<script setup>
import { ref, reactive, onMounted } from 'vue'
import Hls from 'hls.js';
import DPlayer from 'dplayer';
console.log(DPlayer);
let dpInstance = null
function playVideo() {
console.log(dpInstance.play);
dpInstance.play()
}
function pauseVideo() {
dpInstance.pause()
}
onMounted(() => {
// 另一种方式,使用 customType
const dp = new DPlayer({
container: document.getElementById('dplayer'),
video: {
url: 'http://127.0.0.1:8085/ts/video/playM3U8video/YsIlFSjnlh',
type: 'customHls',
customType: {
customHls: function (video, player) {
let config = {
xhrSetup: function (xhr, url) {
xhr.withCredentials = true; // 会携带cookie
xhr.setRequestHeader('token', "my-token")
},
}
const hls = new Hls(config);
hls.loadSource(video.src);
hls.attachMedia(video);
},
},
},
});
window.dp = dp
dpInstance = dp
})
script>
<style lang="scss">style>