vue分片上傳視頻并轉(zhuǎn)換為m3u8文件播放的實(shí)現(xiàn)示例
開發(fā)環(huán)境:
基于若依開源框架的前后端分離版本的實(shí)踐,后端java的springboot,前端若依的vue2,做一個(gè)分片上傳視頻并分段播放的功能,因?yàn)槭切№?xiàng)目,并沒(méi)有專門準(zhǔn)備文件服務(wù)器和CDN服務(wù),后端也是套用的若依的上傳功能
實(shí)現(xiàn)思路:
- 前端根據(jù)視頻文件計(jì)算出文件md5值
- 前端按照指定大小截取視頻,執(zhí)行分片上傳(可優(yōu)化,先使用文件MD5檢查文件是否已上傳)
- 后端實(shí)現(xiàn)接收分片的接口,當(dāng)已上傳分片數(shù)等于總分片數(shù)時(shí)執(zhí)行合并分片,得到原視頻文件
- 后端使用ffmpeg按照時(shí)間進(jìn)行視頻分割,切割時(shí)間根據(jù)視頻清晰度不同而不同,得到m3u8文件和ts文件列表
- 后端保存視頻信息和文件實(shí)際保存地址,并提供查詢接口
- 前端使用流播放器播放視頻文件
代碼實(shí)現(xiàn)
1. vue的分片上傳
前端分片上傳功能按照以下步驟實(shí)現(xiàn):
1.1,先要寫一個(gè)上傳組件,這里使用elementUI的上傳組件
在 :auto-upload 設(shè)置的視頻直接不解釋上傳,即選擇好本地文件就上傳
在 :before-upload 中需要計(jì)算好文件的md5值,然后去后端查看文件是否已被上傳
在 :http-request 中實(shí)現(xiàn)具體的分片上傳邏輯
在 :action 雖然設(shè)置了上傳地址,但是任然是以http-request設(shè)置的方法為準(zhǔn),只是不設(shè)置會(huì)報(bào)錯(cuò)
<el-form-item label="視頻文件" prop="file" v-if="form.id==null">
<el-upload ref="upload"
:action="uploadUrl"
:on-error="onError"
:before-upload="beforeUpload"
:before-remove="beforeRemove"
:auto-upload="true"
:limit="1"
:http-request="chunkedUpload"
:on-progress="onProgress"
>
<div style="border: 1px dashed #c0ccda;padding: 1rem;">
<i class="el-icon-upload"></i>
<div class="el-upload__text">將文件拖到此處,或<em>點(diǎn)擊上傳</em></div>
</div>
<div class="el-upload__tip" slot="tip">只能上傳mp4文件,且不超過(guò)500M</div>
<el-progress :percentage="uploadPercentage" status="success"></el-progress>
</el-upload>
</el-form-item>
1.2,上傳方法的js
我使用了兩個(gè)后端接口,
一個(gè)是 testUploadVideo 判斷文件是否存在,是若依分裝的請(qǐng)求
一個(gè)是 process.env.VUE_APP_BASE_API + ‘/manage/video/upload’,單獨(dú)用axios執(zhí)行上傳分片

<script>
import { addVideo, getVideo, testUploadVideo, updateVideo } from '@/api/manage/video'
import SparkMD5 from 'spark-md5'
import axios from 'axios'
export default {
name: 'videoWin',
data() {
return {
uploadUrl: process.env.VUE_APP_BASE_API + '/manage/video/upload', //文件上傳的路徑
uploadPromises: [], // 記錄并發(fā)上傳分片的線程
uploadPercentage:0 //上傳進(jìn)度
}
},,
methods: {
beforeUpload: async function(file) {
// 在上傳之前獲取視頻的寬高和分辨率
const video = document.createElement('video')
video.src = URL.createObjectURL(file)
video.preload = 'metadata'
const loadedMetadata = new Promise(resolve => {
video.onloadedmetadata = () => {
window.URL.revokeObjectURL(video.src)
const width = video.videoWidth
const height = video.videoHeight
console.log('視頻寬高:', width, height)
this.form.width = width
this.form.height = height
resolve();
}
});
// 等待視頻的寬高和分辨率獲取完成
await loadedMetadata;
// 計(jì)算文件的md5值
const reader = new FileReader()
const md5Promise = new Promise(resolve => {
reader.onload = () => {
const spark = new SparkMD5.ArrayBuffer()
spark.append(reader.result)
const md5 = spark.end(false)
this.form.identifier = md5 // 將MD5值存儲(chǔ)到form中
resolve(md5);
}
});
reader.readAsArrayBuffer(file); // 讀取文件內(nèi)容并計(jì)算MD5值
const md5 = await md5Promise;
// 檢查文件是否已被上傳
const response = await testUploadVideo(md5);
console.log("判斷文件是否存在", response)
if (response.msg === "文件已存在,秒傳成功") {
console.log("文件已存在")
// 取消上傳
this.$refs.upload.abort(file);
return false;
} else {
return true;
}
},
chunkedUpload({ file }) {
const totalSize = file.size
const chunkCount = Math.ceil(totalSize / (5 * 1024 * 1024)) // 每個(gè)分片5MB
// 創(chuàng)建分片上傳請(qǐng)求數(shù)組
// 上傳分片
for (let i = 0; i < chunkCount; i++) {
const start = i * (5 * 1024 * 1024)
const end = Math.min((i + 1) * (5 * 1024 * 1024), totalSize)
const chunk = file.slice(start, end)
const formData = new FormData()
formData.append('file', chunk)
formData.append('filename', file.name)
formData.append('totalChunks', chunkCount)
formData.append('chunkNumber', i)
formData.append('identifier', this.form.identifier) // 添加文件的MD5值作為參數(shù)
// 發(fā)送分片上傳請(qǐng)求
const source = axios.CancelToken.source() // 創(chuàng)建cancelToken
const uploadPromise = this.uploadChunk(formData, source.token, (progressEvent) => {
console.log('更新進(jìn)度', progressEvent)
this.uploadPercentage = Math.round((progressEvent.loaded / progressEvent.total) * 100) // 更新進(jìn)度條的值;
}).catch(error => {
console.error('分片上傳失敗', error)
// 彈出告警消息
this.$message({
type: 'error',
message: '視頻上傳失??!'
})
})
this.uploadPromises.push({ promise: uploadPromise, source }) // 保存cancelToken
}
// 等待所有分片上傳完成
return Promise.all(this.uploadPromises)
.then(responses => {
console.log('分片上傳完成', responses)
}).catch(error => {
console.error('分片上傳失敗', error)
})
},
/**更新進(jìn)度*/
onProgress(event, file) {
this.uploadPercentage = Math.floor((event.loaded / event.total) * 100);
},
/**上傳分片*/
uploadChunk(formData, onProgress) {
return axios.post(process.env.VUE_APP_BASE_API + '/manage/video/upload', formData, {
onUploadProgress: onProgress // 添加進(jìn)度回調(diào)
}).then(response => {
console.log('分片上傳成功', response.data)
})
},
/**上傳分片失敗*/
onError(error, file, fileList) {
console.error('上傳失敗', error)
},
// 取消上傳請(qǐng)求
beforeRemove(file, fileList) {
this.form.identifier = null
return true
}
}
}
</script>
2. 后端接口實(shí)現(xiàn)
2.1 控制層代碼
@RestController
@RequestMapping("/manage/video")
@CrossOrigin // 允許跨域
public class ManageVideoController extends BaseController {
@Autowired
private IManageVideoService manageVideoService;
/**
* 上傳分片前校驗(yàn)文件是否存在
*
* @return
*/
@GetMapping("/preUpload")
public AjaxResult preUpload(@RequestParam("fileMd5") String fileMd5) {
return manageVideoService.checkExists(fileMd5);
}
/**
* 上傳分片
*
* @return
*/
@PostMapping("/upload")
public AjaxResult fragmentation(@ModelAttribute UploadPO uploadPO) {
return manageVideoService.uploadChunk(uploadPO);
}
}
2.2 服務(wù)層代碼
接收到分片上傳文件后經(jīng)歷以下步驟:
- 再次校驗(yàn)是否文件已存在,不存在就保存臨時(shí)分片文件;
- 校驗(yàn)已上傳分片數(shù)是否等于總分篇數(shù),如果是則合并;
- 將臨時(shí)文件合并和源mp4文件;
- 獲取視頻的時(shí)長(zhǎng)和大小,因?yàn)閒fmpeg不支持按照大小拆分,如果只是按照固定時(shí)長(zhǎng)拆分,20s可能是2M也可能是34M,無(wú)法達(dá)到拆分視頻以縮短預(yù)覽視頻等待時(shí)間的目的;
- 執(zhí)行視頻拆分,生成playlist.m3u8和一系列ts文件
- 重寫m3u8文件的ts地址,1是因?yàn)槿粢篱_發(fā)環(huán)境和線上環(huán)境的指定前綴不一致,2是因?yàn)楸镜亻_發(fā)沒(méi)開nginx轉(zhuǎn)發(fā)靜態(tài)資源,線上也沒(méi)開文件服務(wù)
@Override
public AjaxResult checkExists(String fileMd5) {
String fileUploadDir = RuoYiConfig.getProfile() + "/video";
//判斷文件是否已被上傳
String videoFile = fileUploadDir + "/" + fileMd5 + ".mp4";
File file = new File(videoFile);
if (file.exists()) {
return AjaxResult.success("文件已存在,秒傳成功");
}
return AjaxResult.success();
}
@Override
public AjaxResult uploadChunk(UploadPO uploadPO) {
String fileUploadTempDir = RuoYiConfig.getProfile() + "/videotmp";
String fileUploadDir = RuoYiConfig.getProfile() + "/video";
// 獲得文件分片數(shù)據(jù)
MultipartFile fileData = uploadPO.getFile();
// 分片第幾片
int index = uploadPO.getChunkNumber();
//總分片數(shù)
int totalChunk = uploadPO.getTotalChunks();
// 文件md5標(biāo)識(shí)
String fileMd5 = uploadPO.getIdentifier();
//判斷文件是否已被上傳
String videoFile = fileUploadDir + "/" + fileMd5 + ".mp4";
File file = new File(videoFile);
if (file.exists()) {
return AjaxResult.success("文件已存在,秒傳成功");
}
String newName = fileMd5 + index + ".tem";
File uploadFile = new File(fileUploadTempDir + "/" + fileMd5, newName);
if (!uploadFile.getParentFile().exists()) {
uploadFile.getParentFile().mkdirs();
}
try {
fileData.transferTo(uploadFile);
// 判斷總分片數(shù)是否等于當(dāng)前目錄下的分片文件數(shù)量
int currentChunkCount = getChunkCount(fileUploadTempDir + "/" + fileMd5);
if (totalChunk == currentChunkCount) {
// 調(diào)用合并方法
merge(fileMd5, fileUploadTempDir, fileUploadDir);
//根據(jù)運(yùn)行環(huán)境分別調(diào)用ffmpeg
String os = System.getProperty("os.name").toLowerCase();
String m3u8Dir = fileUploadDir + "/" + fileMd5;
File m3u8FileDir = new File(m3u8Dir);
if (!m3u8FileDir.exists()) {
m3u8FileDir.mkdirs();
}
//計(jì)算視頻總時(shí)長(zhǎng)和視頻大小,確定視頻的分段時(shí)長(zhǎng)
String mp4File = fileUploadDir + "/" + fileMd5 + ".mp4";
//每個(gè)2M分片的毫秒數(shù)
long duration = getTsDuration(mp4File);
// 異步執(zhí)行視頻拆分
if (os.contains("win")) {
mp4ToM3u8ForWindow(fileMd5, mp4File, m3u8Dir, duration);
} else {
mp4ToM3u8ForLinux(fileMd5, mp4File, m3u8Dir, duration);
}
}
//執(zhí)行成功返回 url
return AjaxResult.success();
} catch (IOException | InterruptedException e) {
log.error("上傳視頻失敗:{}", e.toString());
FileUtil.del(fileUploadTempDir + "/" + fileMd5); //刪除臨時(shí)文件
FileUtil.del(videoFile); //刪除視頻源文件
FileUtil.del(fileUploadDir + "/" + fileMd5); //刪除分段ts視頻
return AjaxResult.error(502, "上傳視頻失敗");
} catch (EncoderException e) {
log.error("視頻切割時(shí)計(jì)算分段時(shí)長(zhǎng)失?。簕}", e.toString());
FileUtil.del(fileUploadTempDir + "/" + fileMd5); //刪除臨時(shí)文件
FileUtil.del(videoFile); //刪除視頻源文件
FileUtil.del(fileUploadDir + "/" + fileMd5); //刪除分段ts視頻
return AjaxResult.error(502, "上傳視頻失敗");
}
}
/**
* 獲取當(dāng)前目錄下的分片文件數(shù)量
*
* @param directoryPath
* @return
*/
private int getChunkCount(String directoryPath) {
File directory = new File(directoryPath);
if (!directory.exists() || !directory.isDirectory()) {
return 0;
}
File[] files = directory.listFiles((dir, name) -> name.endsWith(".tem"));
return files != null ? files.length : 0;
}
/**
* 合并分片
*
* @param uuid
* @return
*/
public void merge(String uuid, String fileUploadTempDir, String fileUploadDir) throws IOException {
File dirFile = new File(fileUploadTempDir + "/" + uuid);
//分片上傳的文件已經(jīng)位于同一個(gè)文件夾下,方便尋找和遍歷(當(dāng)文件數(shù)大于十的時(shí)候記得排序用冒泡排序確保順序是正確的)
String[] fileNames = dirFile.list();
Arrays.sort(fileNames, (o1, o2) -> {
int i1 = Integer.parseInt(o1.substring(o1.indexOf(uuid) + uuid.length()).split("\\.tem")[0]);
int i2 = Integer.parseInt(o2.substring(o2.indexOf(uuid) + uuid.length()).split("\\.tem")[0]);
return i1 - i2;
});
//創(chuàng)建空的合并文件,以未見md5為文件名
File targetFile = new File(fileUploadDir, uuid + ".mp4");
if (!targetFile.getParentFile().exists()) {
targetFile.getParentFile().mkdirs();
}
RandomAccessFile writeFile = new RandomAccessFile(targetFile, "rw");
long position = 0;
for (String fileName : fileNames) {
System.out.println(fileName);
File sourceFile = new File(fileUploadTempDir + "/" + uuid, fileName);
RandomAccessFile readFile = new RandomAccessFile(sourceFile, "rw");
int chunksize = 1024 * 3;
byte[] buf = new byte[chunksize];
writeFile.seek(position);
int byteCount;
while ((byteCount = readFile.read(buf)) != -1) {
if (byteCount != chunksize) {
byte[] tempBytes = new byte[byteCount];
System.arraycopy(buf, 0, tempBytes, 0, byteCount);
buf = tempBytes;
}
writeFile.write(buf);
position = position + byteCount;
}
readFile.close();
}
writeFile.close();
cn.hutool.core.io.FileUtil.del(dirFile);
}
/**
* 視頻拆分
*
* @param inputFilePath D:/home/dxhh/uploadPath/video/md5.mp4
* @param outputDirectory D:/home/dxhh/uploadPath/video/md5
*/
@Async
public void mp4ToM3u8ForWindow(String fileMd5, String inputFilePath, String outputDirectory, long ms) throws IOException {
File uploadFile = new File(outputDirectory);
if (!uploadFile.exists()) {
uploadFile.mkdirs();
}
Path outputDirPath = Paths.get(outputDirectory);
//我的ffmpeg.exe放在 項(xiàng)目的/resources/script目錄下
Path resourcePath = Paths.get("./script/ffmpeg.exe");
FFmpeg.atPath(resourcePath.getParent())
.addInput(UrlInput.fromPath(Paths.get(inputFilePath)))
.addOutput(UrlOutput.toPath(outputDirPath.resolve("output_%03d.ts")))
.addArguments("-f", "segment")
.addArguments("-segment_time", ms + "ms") // 分片時(shí)長(zhǎng)為30s
.addArguments("-segment_list", outputDirPath.resolve("playlist.m3u8").toString())
.addArguments("-c:v", "copy") // 優(yōu)化視頻編碼參數(shù)
.addArguments("-c:a", "copy") // 優(yōu)化音頻編碼參數(shù)
.execute();
// 修改生成的m3u8文件,將ts鏈接替換為完整URL
updateM3u8File(fileMd5, outputDirectory);
}
/**
* 視頻拆分
*
* @param fileMd5 adw1dwdadadwdadasd
* @param inputFilePath /home/dxhh/uploadPath/video/md5.mp4
* @param outputDirectory /home/dxhh/uploadPath/video/md5
* @throws IOException
* @throws InterruptedException
*/
public void mp4ToM3u8ForLinux(String fileMd5, String inputFilePath, String outputDirectory, long ms) throws IOException, InterruptedException {
String command = "ffmpeg -i " + inputFilePath + " -c copy -map 0 -f segment -segment_time " + ms + "ms -segment_list " + outputDirectory + "/playlist.m3u8 " + outputDirectory + "/output_%03d.ts";
//ffmpeg -i /home/dxhh/uploadPath/video/md5.mp4 -c copy -map 0 -f segment -segment_time 1236ms -segment_list /home/dxhh/uploadPath/video/md5/playlist.m3u8 /home/dxhh/uploadPath/video/md5/output_%03d.ts
log.info("視頻分割腳本:{}", command);
ProcessBuilder builder = new ProcessBuilder(command.split(" "));
builder.redirectErrorStream(true);
Process process = builder.start();
BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()));
String line;
while ((line = reader.readLine()) != null) {
System.out.println(line);
}
int exitCode = process.waitFor();
if (exitCode == 0) {
System.out.println("FFmpeg command executed successfully");
updateM3u8File(fileMd5, outputDirectory);
} else {
System.out.println("FFmpeg command failed with exit code " + exitCode);
}
}
private void updateM3u8File(String fileMd5, String outputDirectory) throws IOException {
String m3u8FilePath = outputDirectory + "/playlist.m3u8";
List<String> lines = Files.readAllLines(Paths.get(m3u8FilePath));
List<String> newLines = new ArrayList<>();
for (String line : lines) {
if (line.endsWith(".ts")) {
if ("dev".equals(active)) {
newLines.add("/dev-api/profile/video/" + fileMd5 + "/" + line);
} else {
newLines.add("/stage-api/profile/video/" + fileMd5 + "/" + line);
}
} else {
newLines.add(line);
}
}
Files.write(Paths.get(m3u8FilePath), newLines);
}
public long getTsDuration(String filePath) throws EncoderException {
int targetSize = 2 * 1024 * 1024; // 2MB
File videoFile = new File(filePath);
long fileSize = videoFile.length();
Encoder encoder = new Encoder();
MultimediaInfo multimediaInfo = encoder.getInfo(videoFile);
long duration = multimediaInfo.getDuration();
System.out.println("Duration: " + duration + " ms");
System.out.println("File size: " + fileSize + " bytes");
// Calculate target duration for a 2MB video
long targetDuration = (duration * targetSize) / fileSize;
System.out.println("Target duration for a 2MB video: " + targetDuration + " ms");
return targetDuration;
}
獲取視頻時(shí)長(zhǎng)需要用到j(luò)ave工具包,想上傳資源的提示已存在,應(yīng)該可以在csdn搜到;
還需要ffmpeg軟件,如果是windows環(huán)境運(yùn)行,只需要調(diào)用本地的ffmpeg.exe就好,如果是在linux運(yùn)行,需要安裝ffmpeg;
<!--視頻切割-->
<dependency>
<groupId>com.github.kokorin.jaffree</groupId>
<artifactId>jaffree</artifactId>
<version>2023.09.10</version>
</dependency>
<dependency>
<groupId>it.sauronsoftware.jave</groupId>
<artifactId>jave2</artifactId>
<version>1.0.2</version>
<scope>system</scope>
<systemPath>${project.basedir}/lib/jave-1.0.2.jar</systemPath>
</dependency>
2.3 linux中安裝ffmpeg
- 下載 ffmpeg 工具包并解壓
wget http://www.ffmpeg.org/releases/ffmpeg-4.2.tar.gz tar -zxvf ffmpeg-4.2.tar.gz
- 進(jìn)入工具包文件夾并進(jìn)行安裝,將 ffmpeg 安裝至 / usr/local/ffmpeg 下
cd ffmpeg-4.2 ./configure --prefix=/usr/local/ffmpeg ./configure --prefix=/usr/local/ffmpeg --enable-openssl --disable-x86asm make && make install
注意:若出現(xiàn)以下報(bào)錯(cuò),請(qǐng)?zhí)恋谖宀?,待第五步安裝成功后再返回第二步。

- 配置環(huán)境變量,使其 ffmpeg 命令生效
#利用vi編輯環(huán)境變量 vi /etc/profile #在最后位置處添加環(huán)境變量,點(diǎn)擊i進(jìn)入編輯模式,esc鍵可退出編輯模式 export PATH=$PATH:/usr/local/ffmpeg/bin #退出編輯模式后,:wq 保存退出 #刷新資源,使其生效 source /etc/profile
- 查看 ffmpeg 版本,驗(yàn)證是否安裝成功
ffmpeg -version
若出現(xiàn)以下內(nèi)容,則安裝成功。

- 若第二步出現(xiàn)圖片中的錯(cuò)誤信息,則需要安裝 yasm
記得退出 ffmpeg 工具包文件夾,cd … 返回上一層
#下載yasm工具包 wget http://www.tortall.net/projects/yasm/releases/yasm-1.3.0.tar.gz #解壓 tar -zxvf yasm-1.3.0.tar.gz #進(jìn)入工具包文件夾并開始安裝 cd yasm-1.3.0 ./configure make && make install
安裝完成后直接返回第二步即可,此時(shí)命令就不會(huì)報(bào)錯(cuò)了。
2.4 視頻資源地址
因?yàn)槭腔谌粢揽蚣荛_發(fā)的,其實(shí)只要上傳的的時(shí)候是往 RuoYiConfig.getProfile() 這個(gè)指定配置目錄保存文件,都是能直接訪問(wèn)不需要額外開發(fā),這里就簡(jiǎn)單過(guò)一下
若依的自定義參數(shù)配置類從yml文件讀取用戶配置
@Component
@ConfigurationProperties(prefix = "xxx")
public class RuoYiConfig {
/**
* 上傳路徑 /home/user/xxxx/upload
*/
private static String profile;
}
在通用配置定義一個(gè)靜態(tài)資源路由前綴
/**
* 通用常量定義
*
* @author li.dh
*/
public class CommonConstant {
/**
* 資源映射路徑 前綴
*/
public static final String RESOURCE_PREFIX = "/profile";
}
在mvc配置中添加靜態(tài)資源的轉(zhuǎn)發(fā)映射,將/profile前綴的請(qǐng)求轉(zhuǎn)發(fā)到RuoYiConfig.getProfile()路徑下
@Configuration
public class ResourcesConfig implements WebMvcConfigurer {
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
/** 本地文件上傳路徑 */
registry.addResourceHandler(CommonConstant.RESOURCE_PREFIX + "/**")
.addResourceLocations("file:" + RuoYiConfig.getProfile() + "/");
/** swagger配置 */
registry.addResourceHandler("/swagger-ui/**")
.addResourceLocations("classpath:/META-INF/resources/webjars/springfox-swagger-ui/")
.setCacheControl(CacheControl.maxAge(5, TimeUnit.HOURS).cachePublic());
}
}
3. vue播放流視頻
我的需求是在列表上點(diǎn)擊視頻彈出播放彈窗
<!-- 播放視頻 -->
<el-dialog :title="title" :visible.sync="open" width="800px" append-to-body @close="open=false">
<video-player class="video-player vjs-custom-skin"
ref="videoPlayer"
:playsinline="true"
:options="playerOptions"
>
</video-player>
</el-dialog>
import 'video.js/dist/video-js.css'
data(){
return {
// 彈出層標(biāo)題
title: '',
m3u8Url: '',
// 是否顯示彈出層
open: false,
playerOptions: {
playbackRates: [0.5, 1.0, 1.5, 2.0], // 可選的播放速度
autoplay: true, // 如果為true,瀏覽器準(zhǔn)備好時(shí)開始回放。
muted: false, // 默認(rèn)情況下將會(huì)消除任何音頻。
loop: false, // 是否視頻一結(jié)束就重新開始。
preload: 'auto', // 建議瀏覽器在<video>加載元素后是否應(yīng)該開始下載視頻數(shù)據(jù)。auto瀏覽器選擇最佳行為,立即開始加載視頻(如果瀏覽器支持)
language: 'zh-CN',
aspectRatio: '16:9', // 將播放器置于流暢模式,并在計(jì)算播放器的動(dòng)態(tài)大小時(shí)使用該值。值應(yīng)該代表一個(gè)比例 - 用冒號(hào)分隔的兩個(gè)數(shù)字(例如"16:9"或"4:3")
fluid: true, // 當(dāng)true時(shí),Video.js player將擁有流體大小。換句話說(shuō),它將按比例縮放以適應(yīng)其容器。
sources: [{
type: 'application/x-mpegURL', // 類型
src: this.m3u8Url
}],
poster: '', // 封面地址
notSupportedMessage: '此視頻暫無(wú)法播放,請(qǐng)稍后再試', // 允許覆蓋Video.js無(wú)法播放媒體源時(shí)顯示的默認(rèn)信息。
controlBar: {
timeDivider: true, // 當(dāng)前時(shí)間和持續(xù)時(shí)間的分隔符
durationDisplay: true, // 顯示持續(xù)時(shí)間
remainingTimeDisplay: false, // 是否顯示剩余時(shí)間功能
fullscreenToggle: true // 是否顯示全屏按鈕
}
}
}
},
methods: {
openVideo(picurl, url, title) {
this.title = title
let videourl = process.env.VUE_APP_BASE_API + url
let imgurl = process.env.VUE_APP_BASE_API + picurl
// console.log("視頻地址:" , videourl)
this.m3u8Url = videourl
this.playerOptions.sources[0].src = videourl // 重新加載視頻
this.playerOptions.poster = imgurl // 封面
// this.$refs.videoPlayer.play() // 播放視頻
this.open = true
}
}
4. 實(shí)現(xiàn)效果

到此這篇關(guān)于vue分片上傳視頻并轉(zhuǎn)換為m3u8文件播放的實(shí)現(xiàn)示例的文章就介紹到這了,更多相關(guān)vue分片上傳視頻內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
vue中計(jì)算屬性(computed)、methods和watched之間的區(qū)別
這篇文章主要給大家介紹了關(guān)于vue中計(jì)算屬性(computed)、methods和watched之間區(qū)別的相關(guān)資料,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面跟著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧。2017-07-07
Vue.js中該如何自己維護(hù)路由跳轉(zhuǎn)記錄
這篇文章主要給大家介紹了關(guān)于Vue.js中該如何自己維護(hù)路由跳轉(zhuǎn)記錄的相關(guān)資料,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家學(xué)習(xí)或者使用Vue.js具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2019-05-05
vue.js學(xué)習(xí)之UI組件開發(fā)教程
前端開發(fā)中,隨著業(yè)務(wù)的增多,出于效率的考慮,我們對(duì)于組件化開發(fā)的需求也越來(lái)越迫切。下面這篇文章主要給大家介紹了關(guān)于vue.js之UI組件開發(fā)的相關(guān)資料,文中介紹的非常詳細(xì),需要的朋友們下面來(lái)一起看看吧。2017-07-07
Vue模擬實(shí)現(xiàn)購(gòu)物車結(jié)算功能
這篇文章主要為大家詳細(xì)介紹了Vue模擬實(shí)現(xiàn)購(gòu)物車結(jié)算功能,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2021-09-09
用vue設(shè)計(jì)一個(gè)數(shù)據(jù)采集器
這篇文章主要介紹了如何用vue設(shè)計(jì)一個(gè)數(shù)據(jù)采集器,幫助大家更好的理解和學(xué)習(xí)使用vue框架,感興趣的朋友可以了解下2021-04-04
vue上傳文件formData入?yún)榭?接口請(qǐng)求500的解決
這篇文章主要介紹了vue上傳文件formData入?yún)榭?接口請(qǐng)求500的解決,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-06-06

