在线帮助中心对视频加载,过程优化,降低视频对服务端的负载

1. 背景

大数据平台XSailboat 中包含 帮助中心 模块,提供在线的帮助文档和平台使用教程。

在帮助中心,不仅支持普通的文字,图片,还希望支持视频。
帮助中心文档中包视频
前端网页显示出视频数据,在大数据平台的软件架构下,会经历这样的数据链路:
视频的数据链路
在用户点击目录,打开文档时,其实他不一定会去看视频,为了提升效率,希望在用户点击观看视频的时候,才去加载视频。

在浏览器端,显示视频使用的是video字段。在默认情况下,当页面dom加载出来之后,会自动去加载视频数据。为了不让它自动加载,我们刚开始尝试了video的preload属性。它有auto、metadata、none三个属性可选。

  • auto,自动加载整个视频,
  • metadata,会加入头部一段视频之后刮起,等人去点击播放又重新打开一个流去读取
  • none,不加载视频,视频区域看起来一片黑

2. 实现方案

实现思路:

  1. 在视频类型的附件上传的时候,中台服务在往数据库里面写之前,从视频内容里面截取一帧图片。这样会有两个附件:一个视频附件,一个图片附件。从视频中提取出来的图片附件,它的id为“视频附件的id.poster”,类型为jpg。
  2. 前端显示的时候,设置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特性的,这样就能实现预览一部分数据,提取一帧图片,然后回到头上重新开始,将视频数据完整写入到数据库。
以下代码供参考:

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包集合
在这里插入图片描述

  • 0
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值