1. 背景
在 大数据平台XSailboat 中包含 帮助中心 模块,提供在线的帮助文档和平台使用教程。
在帮助中心,不仅支持普通的文字,图片,还希望支持视频。
前端网页显示出视频数据,在大数据平台的软件架构下,会经历这样的数据链路:
在用户点击目录,打开文档时,其实他不一定会去看视频,为了提升效率,希望在用户点击观看视频的时候,才去加载视频。
在浏览器端,显示视频使用的是video字段。在默认情况下,当页面dom加载出来之后,会自动去加载视频数据。为了不让它自动加载,我们刚开始尝试了video的preload属性。它有auto、metadata、none三个属性可选。
- auto,自动加载整个视频,
- metadata,会加入头部一段视频之后刮起,等人去点击播放又重新打开一个流去读取
- none,不加载视频,视频区域看起来一片黑
2. 实现方案
实现思路:
- 在视频类型的附件上传的时候,中台服务在往数据库里面写之前,从视频内容里面截取一帧图片。这样会有两个附件:一个视频附件,一个图片附件。从视频中提取出来的图片附件,它的id为“视频附件的id.poster”,类型为jpg。
- 前端显示的时候,设置video的poster属性,这个poster的链接地址就是步骤1提取到的附件地址。
3. 如何从视频中获得一帧poster(海报)
这里就需要使用java-cv库。这里的cv是 Computer Vision(计算机视觉)的缩写。
<dependency>
<groupId>org.bytedeco</groupId>
<artifactId>javacv-platform</artifactId>
<version>1.5.9</version>
</dependency>
这个库依赖的东西是很多的,在导出的时候,可以只保留用到的那部分jar包。(后面给出最小jar包集合)
视频数据会以输入流(InputStream)的形式到达后台,在不用提取一帧图片的情况下,直接对接输入流和入库(Blob,这里后端数据库是PostgreSQL)的输出流即可。Spring JPA的代码如下:
// 省略...
@Column(name="file_data" )
@Comment("文件内容")
@Basic(fetch = FetchType.LAZY)
Blob fileData ;
public void setContent(InputStream aContent , long aLength)
{
// fileData是附件类的一个属性,类型是java.sql.Blob
fileData = BlobProxy.generateProxy(aContent, aLength) ;
}
// 省略...
现在需要从流中提取出一帧图片,但对于Http连接的输入流,读了数据是无法回撤重读的。即大多数的流其实并不支持mark/reset特性。某个流是否支持mark/reset特性,调用InputStream的这个方法测试一下即可:
/**
* Tests if this input stream supports the <code>mark</code> and
* <code>reset</code> methods. Whether or not <code>mark</code> and
* <code>reset</code> are supported is an invariant property of a
* particular input stream instance. The <code>markSupported</code> method
* of <code>InputStream</code> returns <code>false</code>.
*
* @return <code>true</code> if this stream instance supports the mark
* and reset methods; <code>false</code> otherwise.
* @see java.io.InputStream#mark(int)
* @see java.io.InputStream#reset()
*/
public boolean markSupported() {
// 省略...
}
如果一个流不支持mark/reset特性,可以通过BufferedInputStream包装一下,BufferedInputStream是支持mark/reset特性的,这样就能实现预览一部分数据,提取一帧图片,然后回到头上重新开始,将视频数据完整写入到数据库。
以下代码供参考:文章来源:https://www.toymoban.com/news/detail-823092.html
public String createAttachment(String aDocId,
InputStream aIns,
long aOriginalFileSize,
String aMediaType,
String aOriginalFileName,
String aUserId) throws IOException
{
boolean isVideo = aMediaType.toLowerCase().startsWith("video/") ;
byte[] pic = null ;
InputStream ins = aIns ;
if(isVideo)
{
if(!ins.markSupported())
ins = new BufferedInputStream(ins , 1024_000) ;
ins.mark(1024_000) ;
byte[] buf = new byte[1024_000] ;
int len = 0 ;
int readLen = 0 ;
// 一次可能读不满buf的,得多次读取
while(len < buf.length && (readLen = ins.read(buf , len , buf.length - len)) != -1)
{
len += readLen ;
}
ins.reset() ;
ByteArrayInputStream bins = new ByteArrayInputStream(buf, 0, len) ;
ByteArrayOutputStream bouts = new ByteArrayOutputStream(512_000);
// FFmpegLogCallback.set() ;
getVideoPic(bins, bouts) ;
pic = bouts.toByteArray() ;
}
Date now = new Date() ;
DocAttachment athm = new DocAttachment();
athm.setDocId(aDocId);
athm.setContent(ins , aOriginalFileSize);
athm.setFileName(aOriginalFileName);
athm.setMediaType(aMediaType);
athm.setLength(aOriginalFileSize);
athm.setCreateUserId(aUserId);
athm.setCreateTime(now) ;
String id = mDocAthmRepo.save(athm).getId();
if(isVideo)
{
String picId = id+".poster" ;
athm = new DocAttachment();
athm.setId(picId) ;
athm.setDocId(aDocId);
athm.setContent(new ByteArrayInputStream(pic) , pic.length);
athm.setFileName(aOriginalFileName+".poster");
athm.setMediaType(MediaType.IMAGE_JPEG_VALUE) ;
athm.setLength(pic.length);
athm.setCreateUserId(aUserId);
athm.setCreateTime(now) ;
mDocAthmRepo.save(athm) ;
}
return id ;
}
public static void getVideoPic(InputStream aIns, OutputStream aOuts) throws IOException
{
try(FFmpegFrameGrabber ff = new FFmpegFrameGrabber(aIns)
; Java2DFrameConverter converter = new Java2DFrameConverter())
{
ff.start();
// 截取中间帧图片(具体依实际情况而定)
int i = 0;
int length = ff.getLengthInFrames();
Assert.isTrue(length > 0, "%d 字节的视频数据,没有一帧完整图像!");
Frame frame = null;
while (i < length)
{
frame = ff.grabFrame();
if (frame.image != null)
break;
i++;
}
// 截取的帧图片
BufferedImage srcImage = converter.getBufferedImage(frame);
ff.stop() ;
int srcImageWidth = srcImage.getWidth();
int srcImageHeight = srcImage.getHeight();
// 对截图进行等比例缩放(缩略图)
int width = 480;
int height = (int) (((double) width / srcImageWidth) * srcImageHeight);
BufferedImage thumbnailImage = new BufferedImage(width, height, BufferedImage.TYPE_3BYTE_BGR);
thumbnailImage .getGraphics()
.drawImage(srcImage.getScaledInstance(width, height, Image.SCALE_SMOOTH), 0, 0, null);
ImageIO.write(thumbnailImage, "jpg", aOuts);
}
}
4. java-cv最小jar包集合
完成提取一帧图像所需的最小jar包集合
文章来源地址https://www.toymoban.com/news/detail-823092.html
到了这里,关于在线帮助中心对视频加载,过程优化,降低视频对服务端的负载的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!