概述
视频上传到本地之后(此处可分片上传到本地,然后合并),使用ffmpeg对视频处理成m3u8文件,暂时只测试了avi和mp4格式的文件。
代码
pom.xml
<project xmlns="http://maven.apache.org/pom/4.0.0" xmlns:xsi="http://www.w3.org/2001/xmlschema-instance" xsi:schemalocation="http://maven.apache.org/pom/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelversion>4.0.0</modelversion> <groupid>io.springboot</groupid> <artifactid>springboot-ffmpeg-demo</artifactid> <version>0.0.1-snapshot</version> <parent> <groupid>org.springframework.boot</groupid> <artifactid>spring-boot-starter-parent</artifactid> <version>2.5.4</version> <relativepath /> <!-- lookup parent from repository --> </parent> <dependencies> <dependency> <groupid>org.springframework.boot</groupid> <artifactid>spring-boot-starter-test</artifactid> <scope>test</scope> </dependency> <dependency> <groupid>org.junit.vintage</groupid> <artifactid>junit-vintage-engine</artifactid> <scope>test</scope> </dependency> <dependency> <groupid>org.springframework.boot</groupid> <artifactid>spring-boot-starter-web</artifactid> <exclusions> <exclusion> <groupid>org.springframework.boot</groupid> <artifactid>spring-boot-starter-tomcat</artifactid> </exclusion> </exclusions> </dependency> <dependency> <groupid>org.springframework.boot</groupid> <artifactid>spring-boot-starter-undertow</artifactid> </dependency> <dependency> <groupid>commons-codec</groupid> <artifactid>commons-codec</artifactid> </dependency> <dependency> <groupid>com.google.code.gson</groupid> <artifactid>gson</artifactid> </dependency> </dependencies> <build> <finalname>${project.artifactid}</finalname> <plugins> <plugin> <groupid>org.springframework.boot</groupid> <artifactid>spring-boot-maven-plugin</artifactid> <configuration> <executable>true</executable> </configuration> </plugin> </plugins> </build> </project>
ffmpeg
ffmpegutils
package com.demo.ffmpeg; import java.io.bufferedreader; import java.io.file; import java.io.ioexception; import java.io.inputstreamreader; import java.nio.charset.standardcharsets; import java.nio.file.files; import java.nio.file.path; import java.nio.file.paths; import java.nio.file.standardopenoption; import java.security.nosuchalgorithmexception; import java.util.arraylist; import java.util.list; import javax.crypto.keygenerator; import org.apache.commons.codec.binary.hex; import org.slf4j.logger; import org.slf4j.loggerfactory; import org.springframework.util.stringutils; import com.google.gson.gson; public class ffmpegutils { private static final logger logger = loggerfactory.getlogger(ffmpegutils.class); // 跨平台换行符 private static final string line_separator = system.getproperty("line.separator"); /** * 生成随机16个字节的aeskey * @return */ private static byte[] genaeskey () { try { keygenerator keygenerator = keygenerator.getinstance("aes"); keygenerator.init(128); return keygenerator.generatekey().getencoded(); } catch (nosuchalgorithmexception e) { return null; } } /** * 在指定的目录下生成key_info, key文件,返回key_info文件 * @param folder * @throws ioexception */ private static path genkeyinfo(string folder) throws ioexception { // aes 密钥 byte[] aeskey = genaeskey(); // aes 向量 string iv = hex.encodehexstring(genaeskey()); // key 文件写入 path keyfile = paths.get(folder, "key"); files.write(keyfile, aeskey, standardopenoption.create, standardopenoption.truncate_existing); // key_info 文件写入 stringbuilder stringbuilder = new stringbuilder(); stringbuilder.append("key").append(line_separator); // m3u8加载key文件网络路径 stringbuilder.append(keyfile.tostring()).append(line_separator); // ffmeg加载key_info文件路径 stringbuilder.append(iv); // ase 向量 path keyinfo = paths.get(folder, "key_info"); files.write(keyinfo, stringbuilder.tostring().getbytes(), standardopenoption.create, standardopenoption.truncate_existing); return keyinfo; } /** * 指定的目录下生成 master index.m3u8 文件 * @param filename master m3u8文件地址 * @param indexpath 访问子index.m3u8的路径 * @param bandwidth 流码率 * @throws ioexception */ private static void genindex(string file, string indexpath, string bandwidth) throws ioexception { stringbuilder stringbuilder = new stringbuilder(); stringbuilder.append("#extm3u").append(line_separator); stringbuilder.append("#ext-x-stream-inf:bandwidth=" + bandwidth).append(line_separator); // 码率 stringbuilder.append(indexpath); files.write(paths.get(file), stringbuilder.tostring().getbytes(standardcharsets.utf_8), standardopenoption.create, standardopenoption.truncate_existing); } /** * 转码视频为m3u8 * @param source 源视频 * @param destfolder 目标文件夹 * @param config 配置信息 * @throws ioexception * @throws interruptedexception */ public static void transcodetom3u8(string source, string destfolder, transcodeconfig config) throws ioexception, interruptedexception { // 判断源视频是否存在 if (!files.exists(paths.get(source))) { throw new illegalargumentexception("文件不存在:" + source); } // 创建工作目录 path workdir = paths.get(destfolder, "ts"); files.createdirectories(workdir); // 在工作目录生成keyinfo文件 path keyinfo = genkeyinfo(workdir.tostring()); // 构建命令 list<string> commands = new arraylist<>(); commands.add("ffmpeg"); commands.add("-i") ;commands.add(source); // 源文件 commands.add("-c:v") ;commands.add("libx264"); // 视频编码为h264 commands.add("-c:a") ;commands.add("copy"); // 音频直接copy commands.add("-hls_key_info_file") ;commands.add(keyinfo.tostring()); // 指定密钥文件路径 commands.add("-hls_time") ;commands.add(config.gettsseconds()); // ts切片大小 commands.add("-hls_playlist_type") ;commands.add("vod"); // 点播模式 commands.add("-hls_segment_filename") ;commands.add("%06d.ts"); // ts切片文件名称 if (stringutils.hastext(config.getcutstart())) { commands.add("-ss") ;commands.add(config.getcutstart()); // 开始时间 } if (stringutils.hastext(config.getcutend())) { commands.add("-to") ;commands.add(config.getcutend()); // 结束时间 } commands.add("index.m3u8"); // 生成m3u8文件 // 构建进程 process process = new processbuilder() .command(commands) .directory(workdir.tofile()) .start() ; // 读取进程标准输出 new thread(() -> { try (bufferedreader bufferedreader = new bufferedreader(new inputstreamreader(process.getinputstream()))) { string line = null; while ((line = bufferedreader.readline()) != null) { logger.info(line); } } catch (ioexception e) { } }).start(); // 读取进程异常输出 new thread(() -> { try (bufferedreader bufferedreader = new bufferedreader(new inputstreamreader(process.geterrorstream()))) { string line = null; while ((line = bufferedreader.readline()) != null) { logger.info(line); } } catch (ioexception e) { } }).start(); // 阻塞直到任务结束 if (process.waitfor() != 0) { throw new runtimeexception("视频切片异常"); } // 切出封面 if (!screenshots(source, string.join(file.separator, destfolder, "poster.jpg"), config.getposter())) { throw new runtimeexception("封面截取异常"); } // 获取视频信息 mediainfo mediainfo = getmediainfo(source); if (mediainfo == null) { throw new runtimeexception("获取媒体信息异常"); } // 生成index.m3u8文件 genindex(string.join(file.separator, destfolder, "index.m3u8"), "ts/index.m3u8", mediainfo.getformat().getbitrate()); // 删除keyinfo文件 files.delete(keyinfo); } /** * 获取视频文件的媒体信息 * @param source * @return * @throws ioexception * @throws interruptedexception */ public static mediainfo getmediainfo(string source) throws ioexception, interruptedexception { list<string> commands = new arraylist<>(); commands.add("ffprobe"); commands.add("-i") ;commands.add(source); commands.add("-show_format"); commands.add("-show_streams"); commands.add("-print_format") ;commands.add("json"); process process = new processbuilder(commands) .start(); mediainfo mediainfo = null; try (bufferedreader bufferedreader = new bufferedreader(new inputstreamreader(process.getinputstream()))) { mediainfo = new gson().fromjson(bufferedreader, mediainfo.class); } catch (ioexception e) { e.printstacktrace(); } if (process.waitfor() != 0) { return null; } return mediainfo; } /** * 截取视频的指定时间帧,生成图片文件 * @param source 源文件 * @param file 图片文件 * @param time 截图时间 hh:mm:ss.[sss] * @throws ioexception * @throws interruptedexception */ public static boolean screenshots(string source, string file, string time) throws ioexception, interruptedexception { list<string> commands = new arraylist<>(); commands.add("ffmpeg"); commands.add("-i") ;commands.add(source); commands.add("-ss") ;commands.add(time); commands.add("-y"); commands.add("-q:v") ;commands.add("1"); commands.add("-frames:v") ;commands.add("1"); commands.add("-f"); ;commands.add("image2"); commands.add(file); process process = new processbuilder(commands) .start(); // 读取进程标准输出 new thread(() -> { try (bufferedreader bufferedreader = new bufferedreader(new inputstreamreader(process.getinputstream()))) { string line = null; while ((line = bufferedreader.readline()) != null) { logger.info(line); } } catch (ioexception e) { } }).start(); // 读取进程异常输出 new thread(() -> { try (bufferedreader bufferedreader = new bufferedreader(new inputstreamreader(process.geterrorstream()))) { string line = null; while ((line = bufferedreader.readline()) != null) { logger.error(line); } } catch (ioexception e) { } }).start(); return process.waitfor() == 0; } }
mediainfo
package com.demo.ffmpeg; import java.util.list; import com.google.gson.annotations.serializedname; public class mediainfo { public static class format { @serializedname("bit_rate") private string bitrate; public string getbitrate() { return bitrate; } public void setbitrate(string bitrate) { this.bitrate = bitrate; } } public static class stream { @serializedname("index") private int index; @serializedname("codec_name") private string codecname; @serializedname("codec_long_name") private string codeclongame; @serializedname("profile") private string profile; } // ---------------------------------- @serializedname("streams") private list<stream> streams; @serializedname("format") private format format; public list<stream> getstreams() { return streams; } public void setstreams(list<stream> streams) { this.streams = streams; } public format getformat() { return format; } public void setformat(format format) { this.format = format; } }
transcodeconfig
package com.demo.ffmpeg; public class transcodeconfig { private string poster; // 截取封面的时间 hh:mm:ss.[sss] private string tsseconds; // ts分片大小,单位是秒 private string cutstart; // 视频裁剪,开始时间 hh:mm:ss.[sss] private string cutend; // 视频裁剪,结束时间 hh:mm:ss.[sss] public string getposter() { return poster; } public void setposter(string poster) { this.poster = poster; } public string gettsseconds() { return tsseconds; } public void settsseconds(string tsseconds) { this.tsseconds = tsseconds; } public string getcutstart() { return cutstart; } public void setcutstart(string cutstart) { this.cutstart = cutstart; } public string getcutend() { return cutend; } public void setcutend(string cutend) { this.cutend = cutend; } @override public string tostring() { return "transcodeconfig [poster=" + poster + ", tsseconds=" + tsseconds + ", cutstart=" + cutstart + ", cutend=" + cutend + "]"; } }
application.yml
server: port: 80 #logging: # level: # "root": debug app: # 存储转码视频的文件夹 video-folder: d:\video spring: servlet: multipart: enabled: true # 不限制文件大小 max-file-size: -1 # 不限制请求体大小 max-request-size: -1 # 临时io目录 location: "${java.io.tmpdir}" # 不延迟解析 resolve-lazily: false # 超过1mb,就io到临时目录 file-size-threshold: 1mb web: resources: static-locations: - "classpath:/static/" - "file:${app.video-folder}" # 把视频文件夹目录,添加到静态资源目录列表
application
@springbootapplication public class application { public static void main(string[] args) { springapplication.run(application.class, args); } }
uploadcontroller
package com.demo.web.controller; import java.io.ioexception; import java.nio.file.files; import java.nio.file.path; import java.nio.file.paths; import java.time.localdate; import java.time.format.datetimeformatter; import java.util.hashmap; import java.util.map; import org.slf4j.logger; import org.slf4j.loggerfactory; import org.springframework.beans.factory.annotation.value; import org.springframework.http.httpstatus; import org.springframework.http.responseentity; import org.springframework.web.bind.annotation.postmapping; import org.springframework.web.bind.annotation.requestmapping; import org.springframework.web.bind.annotation.requestpart; import org.springframework.web.bind.annotation.restcontroller; import org.springframework.web.multipart.multipartfile; import com.demo.ffmpeg.ffmpegutils; import com.demo.ffmpeg.transcodeconfig; @restcontroller @requestmapping("/upload") public class uploadcontroller { private static final logger logger = loggerfactory.getlogger(uploadcontroller.class); @value("${app.video-folder}") private string videofolder; private path tempdir = paths.get(system.getproperty("java.io.tmpdir")); /** * 上传视频进行切片处理,返回访问路径 * @param video * @param transcodeconfig * @return * @throws ioexception */ @postmapping public object upload (@requestpart(name = "file", required = true) multipartfile video, @requestpart(name = "config", required = true) transcodeconfig transcodeconfig) throws ioexception { logger.info("文件信息:title={}, size={}", video.getoriginalfilename(), video.getsize()); logger.info("转码配置:{}", transcodeconfig); // 原始文件名称,也就是视频的标题 string title = video.getoriginalfilename(); // io到临时文件 path tempfile = tempdir.resolve(title); logger.info("io到临时文件:{}", tempfile.tostring()); try { video.transferto(tempfile); // 删除后缀 title = title.substring(0, title.lastindexof(".")); // 按照日期生成子目录 string today = datetimeformatter.ofpattern("yyyymmdd").format(localdate.now()); // 尝试创建视频目录 path targetfolder = files.createdirectories(paths.get(videofolder, today, title)); logger.info("创建文件夹目录:{}", targetfolder); files.createdirectories(targetfolder); // 执行转码操作 logger.info("开始转码"); try { ffmpegutils.transcodetom3u8(tempfile.tostring(), targetfolder.tostring(), transcodeconfig); } catch (exception e) { logger.error("转码异常:{}", e.getmessage()); map<string, object> result = new hashmap<>(); result.put("success", false); result.put("message", e.getmessage()); return responseentity.status(httpstatus.internal_server_error).body(result); } // 封装结果 map<string, object> videoinfo = new hashmap<>(); videoinfo.put("title", title); videoinfo.put("m3u8", string.join("/", "", today, title, "index.m3u8")); videoinfo.put("poster", string.join("/", "", today, title, "poster.jpg")); map<string, object> result = new hashmap<>(); result.put("success", true); result.put("data", videoinfo); return result; } finally { // 始终删除临时文件 files.delete(tempfile); } } }
index.html
在resources/static/index.html
<html lang="en"> <head> <meta charset="utf-8"> <title>title</title> <script src="https://cdn.jsdelivr.net/hls.js/latest/hls.min.js"></script> </head> <body> 选择转码文件: <input name="file" type="file" accept="video/*" onchange="upload(event)"> <hr/> <video id="video" width="500" height="400" controls="controls"></video> </body> <script> const video = document.getelementbyid('video'); function upload (e){ let files = e.target.files if (!files) { return } // todo 转码配置这里固定死了 var transcodeconfig = { poster: "00:00:00.001", // 截取第1毫秒作为封面 tsseconds: 15, cutstart: "", cutend: "" } // 执行上传 let formdata = new formdata(); formdata.append("file", files[0]) formdata.append("config", new blob([json.stringify(transcodeconfig)], {type: "application/json; charset=utf-8"})) fetch('/upload', { method: 'post', body: formdata }) .then(resp => resp.json()) .then(message => { if (message.success){ // 设置封面 video.poster = message.data.poster; // 渲染到播放器 var hls = new hls(); hls.loadsource(message.data.m3u8); hls.attachmedia(video); } else { alert("转码异常,详情查看控制台"); console.log(message.message); } }) .catch(err => { alert("转码异常,详情查看控制台"); throw err }) } </script> </html>
测试
avi格式视频转码m3u8
01-jvm内存与垃圾回收篇概述 // 文件夹名称就是视频标题 |-index.m3u8 // 主m3u8文件,里面可以配置多个码率的播放地址 |-poster.jpg // 截取的封面图片 |-ts // 切片目录 |-index.m3u8 // 切片播放索引 |-key // 播放需要解密的aes key
mp4格式视频转码m3u8
以上就是springboot使用ffmpeg实现m3u8切片转码播放的详细内容,更多关于springboot m3u8转码播放的资料请关注代码网其它相关文章!
发表评论