无需转码,无需FFMPEG,如何从Wowza直播流中获得一个截帧图?
下面是一段代码示例,展示了如何利用一个HTTPProvider从Wowza直播流中获得一个截帧图,它无需使用转码插件(Transcoder Addon),也不需要任何第三方工具(例如FFMPEG)来协助,它自己就可以独立完成这个工作,它实际上利用了原来Transcoder中的解码部分的功能并提供了一个新的API,而无须启用Transcoder功能(相当于Decoder部分已经从Transcoder Addon中分离出来了,当然,这是我的理解)。
注意:
1、这个功能依赖Wowza Streaming Engine 4.5.0及以上版本。
2、如果你用试用授权来测试这个功能,那么你的截帧图上右下角会有一个Wowza logo的水印。当然了,正式授权是不会有这个水印的
下面是这个新API的使用示例:
ThumbnailerRequest request = new ThumbnailerRequest(stream, width, height, fitMode);
request.addPacket(keyFrame);
ThumbnailerResponse response = ThumbnailerUtils.generateThumbnail(vhost, request);
在这个API中,截帧图会以一个RGBA的字节数组形式来返回。
然后,你可以利用
ThumbnailerUtils.nativeImageToBufferedImage(response);方法来讲这个字节数组转化为一个二进制的图像数据流(Java BufferedImage)。
下面是用这个API将其转化一个JPEG或PNG格式的图片的代码示例:
BufferedImage image = ThumbnailerUtils.nativeImageToBufferedImage(response);
if (image != null)
{
WMSLoggerFactory.getLogger(CLASS).info(CLASSNAME+".onHTTPRequest["+applicationStr+"/"+appInstanceStr+"/"+streamName+"]: Image result: format:"+formatStr+" size:"+image.getWidth()+"x"+image.getHeight());
try
{
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ImageIO.write(image, formatStr.equals("jpeg")?"jpg":formatStr, baos);
synchronized(this)
{
outBytes = baos.toByteArray();
}
}
catch(Exception e)
{
WMSLoggerFactory.getLogger(CLASS).error(CLASSNAME+".onHTTPRequest["+applicationStr+"/"+appInstanceStr+"/"+streamName+"] ", e);
}
}
下面是使用这些新API的HTTPProvider的完整代码:
import java.awt.image.*;
import java.io.*;
import java.util.*;
import javax.imageio.*;
import com.wowza.util.*;
import com.wowza.wms.amf.*;
import com.wowza.wms.application.*;
import com.wowza.wms.http.*;
import com.wowza.wms.logging.*;
import com.wowza.wms.stream.*;
import com.wowza.wms.transcoder.httpprovider.*;
import com.wowza.wms.transcoder.model.*;
import com.wowza.wms.transcoder.util.*;
import com.wowza.wms.vhost.*;
public class HTTPProviderGenerateThumbnail extends HTTProvider2Base
{
private static final Class CLASS = HTTPProviderGenerateThumbnail.class;
private static final String CLASSNAME = "HTTPProviderGenerateThumbnail";
public void onHTTPRequest(IVHost vhost, IHTTPRequest req, IHTTPResponse resp)
{
if (!doHTTPAuthentication(vhost, req, resp))
return;
byte[] outBytes = null;
String formatStr = "jpeg";
String fitModeStr = "letterbox";
String cropStr = null;
int[] crop = null;
try
{
while(true)
{
String queryStr = req.getQueryString();
if (queryStr == null)
{
WMSLoggerFactory.getLogger(CLASS).warn(CLASSNAME+".onHTTPRequest: Query string missing");
break;
}
Map queryMap = HTTPUtils.splitQueryStr(queryStr);
String streamName = queryMap.get("streamname");
if (streamName == null)
{
WMSLoggerFactory.getLogger(CLASS).warn(CLASSNAME+".onHTTPRequest: Streamname not specified");
break;
}
String comboAppStr = queryMap.get("application");
if (comboAppStr == null)
{
WMSLoggerFactory.getLogger(CLASS).warn(CLASSNAME+".onHTTPRequest: Application name not specified");
break;
}
if (queryMap.containsKey("fitmode"))
fitModeStr = queryMap.get("fitmode");
if (queryMap.containsKey("fitMode"))
fitModeStr = queryMap.get("fitmode");
if (queryMap.containsKey("format"))
formatStr = queryMap.get("format");
if (formatStr == null)
formatStr = "jpeg";
formatStr = formatStr.trim().toLowerCase();
if (formatStr.equals("jpg"))
formatStr = "jpeg";
String sizeStr = queryMap.get("size");
int width = 0;
int height = 0;
if (sizeStr != null)
{
int cindex = sizeStr.indexOf("x");
try
{
width = Integer.parseInt(sizeStr.substring(0, cindex));
height = Integer.parseInt(sizeStr.substring(cindex+1));
}
catch(Exception e)
{
}
}
if (queryMap.containsKey("crop"))
cropStr = queryMap.get("crop");
if (cropStr != null)
{
String[] parts = cropStr.split(",");
if (parts.length >= 4)
{
crop = new int[4];
for(int i=0;i<4;i++)
{
int cropVal = -1;
try
{
cropVal = Integer.parseInt(parts[i].trim());
}
catch(Exception e)
{
}
if (cropVal > 0)
{
crop[i] = cropVal;
}
}
}
}
String applicationStr = comboAppStr;
String appInstanceStr = IApplicationInstance.DEFAULT_APPINSTANCE_NAME;
int cindex = applicationStr.indexOf("/");
if (cindex > 0)
{
appInstanceStr = applicationStr.substring(cindex+1);
applicationStr = applicationStr.substring(0, cindex);
}
IApplication applicationLoc = vhost.getApplication(applicationStr);
if (applicationLoc == null)
{
WMSLoggerFactory.getLogger(CLASS).warn(CLASSNAME+".onHTTPRequest: Application could not be loaded: "+applicationStr);
break;
}
IApplicationInstance appInstance = applicationLoc.getAppInstance(appInstanceStr);
if (appInstance == null)
{
WMSLoggerFactory.getLogger(CLASS).warn(CLASSNAME+".onHTTPRequest: Application instance could not be loaded: "+applicationStr+"/"+appInstanceStr);
break;
}
IMediaStream stream = appInstance.getStreams().getStream(streamName);
if (stream == null)
{
WMSLoggerFactory.getLogger(CLASS).warn(CLASSNAME+".onHTTPRequest["+applicationStr+"/"+appInstanceStr+"]: Stream not found: "+streamName);
break;
}
AMFPacket keyFrame = stream.getLastKeyFrame();
if (keyFrame == null)
{
WMSLoggerFactory.getLogger(CLASS).warn(CLASSNAME+".onHTTPRequest["+applicationStr+"/"+appInstanceStr+"/"+streamName+"]: No key frame");
break;
}
int fitmode = TranscoderStreamUtils.frameSizeFitModeToId(fitModeStr);
if (fitmode <= 0)
{
WMSLoggerFactory.getLogger(CLASS).warn(CLASSNAME+".onHTTPRequest["+applicationStr+"/"+appInstanceStr+"/"+streamName+"]: Fit mode not recognized defaulting to letterbox: "+fitModeStr);
fitmode = TranscoderStream.FRAMESIZE_FITMODE_LETTERBOX;
}
ThumbnailerRequest request = new ThumbnailerRequest(stream, width, height, fitmode);
request.addPacket(keyFrame);
if (crop != null)
request.setCrop(crop);
ThumbnailerResponse response = ThumbnailerUtils.generateThumbnail(vhost, request);
if (response == null)
{
WMSLoggerFactory.getLogger(CLASS).warn(CLASSNAME+".onHTTPRequest["+applicationStr+"/"+appInstanceStr+"/"+streamName+"]: Thumbnail generation failed");
break;
}
BufferedImage image = ThumbnailerUtils.nativeImageToBufferedImage(response);
if (image != null)
{
WMSLoggerFactory.getLogger(CLASS).info(CLASSNAME+".onHTTPRequest["+applicationStr+"/"+appInstanceStr+"/"+streamName+"]: Image result: format:"+formatStr+" size:"+image.getWidth()+"x"+image.getHeight());
try
{
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ImageIO.write(image, formatStr.equals("jpeg")?"jpg":formatStr, baos);
synchronized(this)
{
outBytes = baos.toByteArray();
}
}
catch(Exception e)
{
WMSLoggerFactory.getLogger(CLASS).error(CLASSNAME+".onHTTPRequest["+applicationStr+"/"+appInstanceStr+"/"+streamName+"] ", e);
}
}
break;
}
}
catch (Exception e)
{
WMSLoggerFactory.getLogger(HTTPTranscoderThumbnail.class).error("HTTPTranscoderThumbnail ", e);
}
try
{
if (outBytes != null)
{
resp.setHeader("Content-Type", "image/"+formatStr);
OutputStream out = resp.getOutputStream();
out.write(outBytes);
}
else
resp.setResponseCode(404);
}
catch (Exception e)
{
WMSLoggerFactory.getLogger(HTTPTranscoderThumbnail.class).error("HTTPTranscoderThumbnail ", e);
}
}
}
这个HTTPProvider需要被部署在Wowza Streaming Engine server 中,你需要将下面的配置添加到
[install-dir]/conf/VHost.xml文件中。
注意:不要将它添加为第一个HTTPProvider,另外,这个配置是针对端口8086 (admin)的:
<HTTPProvider>
<BaseClass>com.wowza.wms.transcoder.thumbnailer.HTTPProviderGenerateThumbnail</BaseClass>
<RequestFilters>thumbnail*</RequestFilters>
<AuthenticationMethod>none</AuthenticationMethod>
</HTTPProvider>
调用这个HTTPProvider的URL的格式如下:
http://[wowza-ip]:8086/thumbnail?application=[app-name]&streamname=[stream-name]&size=[width]x[height]&fitmode=[fitmode]&crop=[left],[right],[top],[bottom]&format=[png,jpg]
- application: Wowza应用名(必需)
- streamname: 流名字(必需)
- size: 图像大小(必需)
- fitmode: 适配模式,让视频帧如何适配你要求的图像大小比例,可选的值包括letterbox, stretch, fit-height, fit-width, match-source, crop, 默认是letterbox
- crop: 从原图中扣图的像素
- format: 图像格式,可选的值包括jpeg和png,默认是jpeg
举例说明:
http://[wowza-ip]:8086/thumbnail?application=live&streamname=myStream&size=640x360&fitmode=letterbox