多协议、性能稳定、丰富API的流媒体服务器软件
无需转码,无需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