课外知识----流媒体
视频播放方案
1、 播放器通过 http协议从http服务器上下载视频文件进行播放问题:必须等到视频下载完才可以播放,不支持快进到某个时间点进行播放
2、 播放器通过rtmp协议连接媒体服务器以实时流方式播放视频 使用rtmp协议需要架设媒体服务器,造价高,对于直播多采用此方案。
3、 播放器使用HLS协议连接http服务器(Nginx、Apache等)实现近实时流方式播放视频,HLS协议规定:基于Http协议,视频封装格式为ts,视频的编码格式为H264,音频编码格式为MP3、AAC或者AC3。
FFmpeg的基本使用
下载:https://www.ffmpeg.org/download.html#build-windows(网页)
使用ffmpeg转换文件格式
将.avi文件格式的视频转换成.mp4文件格式(可以将视频转成 mp4/mp3/gif)
ffmpeg -i lucene.avi lucene.mp4
使用ffmpeg生产m3u8/ts文件
第一步:先将avi视频转成mp4
ffmpeg.exe ‐i lucene.avi ‐c:v libx264 ‐s 1280x720 ‐pix_fmt yuv420p ‐b:a 63k ‐b:v 753k ‐r 18 .\lucene.mp4
- -c:v 视频编码为x264 ,x264编码是H264的一种开源编码格式。
- -s 设置分辨率
- -pix_fmt yuv420p:设置像素采样方式,主流的采样方式有三种,YUV4:4:4,YUV4:2:2,YUV4:2:0,它的作用是 根据采样方式来从码流中还原每个像素点的YUV(亮度信息与色彩信息)值。
- -b 设置码率,-b:a和-b:v分别表示音频的码率和视频的码率,-b表示音频加视频的总码率。码率对一个视频质量有 很大的作用
- -r:帧率,表示每秒更新图像画面的次数,通常大于24肉眼就没有连贯与停顿的感觉了
第二步:将mp4生成m3u8(先将视频转换成mp4,在生成m3u8,并切割ts文件,播放效果比直接avi生成m3u8好) 注意:要有 ‘’-‘’
ffmpeg ‐i lucene.mp4 ‐hls_time 10 ‐hls_list_size 0 ‐hls_segment_filename ./hls/lucene_%05d.ts ./hls/lucene.m3u8
- -hls_time 设置每片的长度,单位为秒
- -hls_list_size n: 保存的分片的数量,设置为0表示保存所有分片
- -hls_segment_filename :每段文件的名称,%05d表示5位数字 生成的效果是:将lucene.mp4视频文件每10秒生成一个ts文件,最后生成一个m3u8文件,m3u8文件是ts的索引 文件。
第三步:需要使用支持HSL协议的播放器播放视频(可以直接播放m3u8这个文件)
测试搭建媒体服务器
下载:video.js:https://github.com/videojs/video.js
videojs-contrib-hls:https://github.com/videojs/videojs-contrib-hls#installation
Nginx媒体服务器(注意需要配置跨域)
server { listen 80; server_name localhost; #charset koi8-r; #access_log logs/host.access.log main; location / { root F:/code/baiduyun/; #autoindex on; add_header Access-Control-Allow-Origin *; add_header Access-Control-Allow-Methods 'GET, POST, OPTIONS'; add_header Access-Control-Allow-Headers 'DNT,X-Mx-ReqToken,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization'; } }
将视频上传到F:/代码/baiduyun/中,在baiduyun目录下创建新目录(hls),将m3u8/ts文件放到新目录中
编写html来获取Nginx媒体服务器中的视频
注意video中的元素不能连在一起(注意有空格也不一定是分开的),可以在一行,否则会报错
<!DOCTYPE html> <html lang="en"> <head> <meta http-equiv="content‐type" content="text/html;charset=utf-8"/> <title>视频播放</title> <!--CDN--> <!-- <link href="https://unpkg.com/video.js/dist/video-js.min.css" rel="stylesheet">--> <!-- <script src="https://unpkg.com/video.js/dist/video.min.js"></script>--> <link rel="stylesheet" href="plugin/video-js.css"> <script src="plugin/video.js"></script> <!--下面这个好像没有用,学习的时候视频添加了--> <!-- <script src="plugin/videojs-contrib-hls.js"></script>--> </head> <body> <video id="my-player" class="video-js" controls width=400 height=400 preload="auto" poster="http://localhost/a.jpg" data-setup='{}'> <source src="http://localhost/hls/lucene.m3u8" type="application/x-mpegURL"/> </video> </body> </html>
JAVA操作
参考 https://blog.csdn.net/HSJ0170/article/details/116455218
<dependency> <groupId>ws.schild</groupId> <artifactId>jave-all-deps</artifactId> <version>3.1.1</version> </dependency>
cmd方式调用ffmpeg(封装):
import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import ws.schild.jave.process.ProcessKiller; import ws.schild.jave.process.ProcessWrapper; import ws.schild.jave.process.ffmpeg.DefaultFFMPEGLocator; /** * * @Description:(cmd方式调用ffmpeg) * @author: HeShengjin * @date: 2021年6月22日 下午5:31:38 * @Copyright: */ public class FfmpegCmd { private static final Logger LOG = LoggerFactory.getLogger(ProcessWrapper.class); /** The process representing the ffmpeg execution. */ private Process ffmpeg = null; /** * A process killer to kill the ffmpeg process with a shutdown hook, useful if the jvm execution * is shutted down during an ongoing encoding process. */ private ProcessKiller ffmpegKiller = null; /** A stream reading from the ffmpeg process standard output channel. */ private InputStream inputStream = null; /** A stream writing in the ffmpeg process standard input channel. */ private OutputStream outputStream = null; /** A stream reading from the ffmpeg process standard error channel. */ private InputStream errorStream = null; /** * Executes the ffmpeg process with the previous given arguments. * * @param destroyOnRuntimeShutdown destroy process if the runtime VM is shutdown * @param openIOStreams Open IO streams for input/output and errorout, should be false when * destroyOnRuntimeShutdown is false too * @param ffmpegCmd windows such as (mp4 transform to mov): * " -i C:\\Users\\hsj\\AppData\\Local\\Temp\\jave\\honer.mp4 -c copy C:\\Users\\hsj\\AppData\\Local\\Temp\\jave\\honer_test.mov " * @throws IOException If the process call fails. */ public void execute(boolean destroyOnRuntimeShutdown, boolean openIOStreams, String ffmpegCmd) throws IOException { DefaultFFMPEGLocator defaultFFMPEGLocator = new DefaultFFMPEGLocator(); StringBuffer cmd = new StringBuffer(defaultFFMPEGLocator.getExecutablePath()); //insert blank for delimiter cmd.append(" "); cmd.append(ffmpegCmd); String cmdStr = String.format("ffmpegCmd final is :%s", cmd.toString()); System.out.println(cmdStr); LOG.info(cmdStr); Runtime runtime = Runtime.getRuntime(); try { ffmpeg = runtime.exec(cmd.toString()); if (destroyOnRuntimeShutdown) { ffmpegKiller = new ProcessKiller(ffmpeg); runtime.addShutdownHook(ffmpegKiller); } if (openIOStreams) { inputStream = ffmpeg.getInputStream(); outputStream = ffmpeg.getOutputStream(); errorStream = ffmpeg.getErrorStream(); } } catch (Exception e) { e.printStackTrace(); } } /** * Returns a stream reading from the ffmpeg process standard output channel. * * @return A stream reading from the ffmpeg process standard output channel. */ public InputStream getInputStream() { return inputStream; } /** * Returns a stream writing in the ffmpeg process standard input channel. * * @return A stream writing in the ffmpeg process standard input channel. */ public OutputStream getOutputStream() { return outputStream; } /** * Returns a stream reading from the ffmpeg process standard error channel. * * @return A stream reading from the ffmpeg process standard error channel. */ public InputStream getErrorStream() { return errorStream; } /** If there's a ffmpeg execution in progress, it kills it. */ public void destroy() { if (inputStream != null) { try { inputStream.close(); } catch (Throwable t) { LOG.warn("Error closing input stream", t); } inputStream = null; } if (outputStream != null) { try { outputStream.close(); } catch (Throwable t) { LOG.warn("Error closing output stream", t); } outputStream = null; } if (errorStream != null) { try { errorStream.close(); } catch (Throwable t) { LOG.warn("Error closing error stream", t); } errorStream = null; } if (ffmpeg != null) { ffmpeg.destroy(); ffmpeg = null; } if (ffmpegKiller != null) { Runtime runtime = Runtime.getRuntime(); runtime.removeShutdownHook(ffmpegKiller); ffmpegKiller = null; } } /** * Return the exit code of the ffmpeg process If the process is not yet terminated, it waits for * the termination of the process * * @return process exit code */ public int getProcessExitCode() { // Make sure it's terminated try { ffmpeg.waitFor(); } catch (InterruptedException ex) { LOG.warn("Interrupted during waiting on process, forced shutdown?", ex); } return ffmpeg.exitValue(); } /**close**/ public void close() { destroy(); } }
cmd方式调用ffmpeg(使用):
import java.io.IOException; import java.io.InputStream; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; /** * * @Description:(cmd方式调用ffmpeg使用) * @author: HeShengjin * @date: 2021年6月22日 下午5:32:31 * @Copyright: */ public class FfmpegCmdTest { //执行成功0,失败1 private static int CODE_SUCCESS = 0; private static int CODE_FAIL = 1; //将荣耀视频测试.mp4转换荣耀视频测试_转码.mov格式 private static String cmd_mp4_2_mov = " -i I:\\荣耀视频测试.mp4 -c copy I:\\荣耀视频测试_转码.mov "; //将荣耀视频测试_转码.mov添加水印(2356899074@qq.com)荣耀视频测试_转码_水印.mov private static String cmd_mov_water = " -i I:\\荣耀视频测试_转码.mov -vf \"drawtext=fontfile=Arial.ttf:text='2356899074@qq.com':y=h-line_h-20:x=(w-text_w)/2:fontsize=34:fontcolor=yellow:shadowy=2\" -b:v 3000k I:\\\\荣耀视频测试_转码_水印.mov "; //多线程 private static int core = Runtime.getRuntime().availableProcessors(); private static ExecutorService pool = new ThreadPoolExecutor(core,//核心 core * 2,//最大 0L,//空闲立即退出 TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>(1024),//无边界阻塞队列 new ThreadPoolExecutor.AbortPolicy()); /** * 第一步:mp4转mov * 第二步:mov添加水印 * @param: @param args * @return: void * @throws */ public static void main(String[] args) { //异步执行,获取执行结果code CompletableFuture<Integer> completableFutureTask = CompletableFuture.supplyAsync(() ->{ return cmdExecut(cmd_mp4_2_mov); }, pool) .thenApplyAsync((Integer code)->{ if(CODE_SUCCESS != code) {return CODE_FAIL;} System.out.println("第一步:mp4转mov,成功!"); Integer codeTmp = cmdExecut(cmd_mov_water); if(CODE_SUCCESS != codeTmp) {return CODE_FAIL;} System.out.println("第二步:mov添加水印,成功!"); return codeTmp; }, pool); //获取执行结果 //code=0表示正常 try { System.out.println(String.format("获取最终执行结果:%s", completableFutureTask.get() == CODE_SUCCESS ? "成功!" : "失败!")); } catch (InterruptedException e) { Thread.currentThread().interrupt(); e.printStackTrace(); } catch (ExecutionException e) { e.printStackTrace(); } } /** * * @Description: (执行ffmpeg自定义命令) * @param: @param cmdStr * @param: @return * @return: Integer * @throws */ public static Integer cmdExecut(String cmdStr) { //code=0表示正常 Integer code = null; FfmpegCmd ffmpegCmd = new FfmpegCmd(); /** * 错误流 */ InputStream errorStream = null; try { //destroyOnRuntimeShutdown表示是否立即关闭Runtime //如果ffmpeg命令需要长时间执行,destroyOnRuntimeShutdown = false //openIOStreams表示是不是需要打开输入输出流: // inputStream = processWrapper.getInputStream(); // outputStream = processWrapper.getOutputStream(); // errorStream = processWrapper.getErrorStream(); ffmpegCmd.execute(false, true, cmdStr); errorStream = ffmpegCmd.getErrorStream(); //打印过程 int len = 0; while ((len=errorStream.read())!=-1){ System.out.print((char)len); } //code=0表示正常 code = ffmpegCmd.getProcessExitCode(); } catch (IOException e) { e.printStackTrace(); } finally { //关闭资源 ffmpegCmd.close(); } //返回 return code; } }