如何在直播流上添加图片和文字?
这篇文章向开发者介绍了如何使用Wowza Transcoder 插件在直播流上放置一个图片。它介绍了创建自定义转码模块、在视频上添加图片和文字、以及实现渐进渐出和动画效果的详细过程。
下面的这些例子代码对开发者来说只是一个开始。通过对这些代码进行扩展,可以实现更多高级功能。
开始
概述
Wowza Transcoder 插件可以为它所创建的每一个帧所调用,并允许开发者用一个图片和文字对这一帧的图像进行修改。
因为Transcoder AddOn会被重复地调用,因此开发者也可以用它在输出流中创建动画效果。
注意: 你可以从这里下载本文的这些例子代码
下载 TranscoderOverlayExampleFiles.zip.
准备工作
建议你首先建立一个能够正常播出的直播流,请参照如下进行:
- 首先,很重要的一点是你最好将服务器进行合适的调优,请阅读性能调优.
- 根据如何为一个直播流配置转码 一步一步进行配置。在创建图层覆盖的Transcoder自定义模块之前,请先对直播流的转码和播放进行测试。请阅读故障排查测试中的Test #1 和Test #2 部分。本文的例子中使用的应用为live 输入流为的stream name 为myStream.
-
将这个例子中的内容文件都拷贝到你的Wowza Media Server的content目录下.
下载TranscoderOverlayExampleFiles.zip后并解压缩,将里面的图片内容目录下的所有图片文件拷贝到你的Wowza Media Server 安装路径的content目录下([install-dir]/content).
注意: 如果你要使用自己的图片文件,请确认要符合Wowza Transcoder 对叠加图片的要求。
创建一个新的Transcoder模块
要在直播流上防止一个图片或文字,你必须创建一个Transcoder模块。开发下面的例子时,使用了Eclipse Integrated Development Environment (IDE) 和 Eclipse Wowza 插件。要了解更多,请下载
Wowza 相关技术文档中的 Wowza Streaming Engine IDE 用户使用手册.
-
创建一个新的项目
- 使用Eclipse IDE 创建一个新的Wowza Media Server Project:
- Project name: ModuleTranscoderOverlayExample
- Wowza Media Server location: 选择Wowza Media Server的安装目录.
- Package: com.wowza.wms.plugin.transcoderoverlays
- Name: ModuleTranscoderOverlayExample
-
在Run > Run Configurations下面,选择ModuleTranscoderOverlayExample.
-
在Arguments tab页, 添加以下VM 参数:
-Dcom.wowza.wms.native.base="win"
-
在配置文件中进行配置
在[install-dir]/conf/live/Application.xml中添加下面的代码:
<Module>
<Name>ModuleTranscoderOverlayExample</Name>
<Description>Example Overlay</Description>
<Class>com.wowza.wms.plugin.transcoderoverlays.ModuleTranscoderOverlayExample</Class>
</Module>
- 启动Wowza Streaming Engine
创建Transcoder 插件模块
overlayExample 使用下面的Wowza基类和接口在视频上放置一个图片:
- IliveStreamTranscoderNotify
- LiveStreamTranscoderActionNotifyBase
- TranscoderVideoDecoderNotifyBase
下面的例子也包括了
OverlayImage和
AnimationEvents 两个类以实现快速的绘画和动画效果。你必须将这两个classes加到项目中.
-
修改 ModuleTranscoderOverlayExample 类.
-
导入下面的类包:
import java.awt.Color;
import java.awt.Font;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import com.wowza.util.SystemUtils;
import com.wowza.wms.application.*;
import com.wowza.wms.amf.*;
import com.wowza.wms.client.*;
import com.wowza.wms.module.*;
import com.wowza.wms.request.*;
import com.wowza.wms.stream.*;
import com.wowza.wms.stream.livetranscoder.ILiveStreamTranscoder;
import com.wowza.wms.stream.livetranscoder.ILiveStreamTranscoderNotify;
import com.wowza.wms.transcoder.model.LiveStreamTranscoder;
import com.wowza.wms.transcoder.model.LiveStreamTranscoderActionNotifyBase;
import com.wowza.wms.transcoder.model.TranscoderSession;
import com.wowza.wms.transcoder.model.TranscoderSessionVideo;
import com.wowza.wms.transcoder.model.TranscoderSessionVideoEncode;
import com.wowza.wms.transcoder.model.TranscoderStream;
import com.wowza.wms.transcoder.model.TranscoderStreamDestination;
import com.wowza.wms.transcoder.model.TranscoderStreamDestinationVideo;
import com.wowza.wms.transcoder.model.TranscoderStreamSourceVideo;
import com.wowza.wms.transcoder.model.TranscoderVideoDecoderNotifyBase;
import com.wowza.wms.transcoder.model.TranscoderVideoOverlayFrame;
-
为这个类添加下面的成员变量.
String graphicName = "logo_${com.wowza.wms.plugin.transcoderoverlays.overlayimage.step}.png";
int overlayIndex = 1;
private IApplicationInstance appInstance = null;
private String basePath = null;
private Object lock = new Object();
图片覆盖的叠加顺序(垂直方向)
overlayIndex定义了相对于在transcode/transcode.xml中定义的遮盖图片,如何画出这个模块中定义的遮盖图片,遮盖图片的叠加顺序(垂直方向)如下:
- 较低数值的Encode及Source
- 较高数值的Encode及Source
- 较低数值Decode及Destination
- 较高数值Decode及Destination
-其中-
- Encode 为transcode/transcode.xml文件中的Root/Transcode/Encodes/Encode/Video/Overlays/Overlay/Index元素的值
- Source 代表这个模块源代码中的encodeSource=true
- Decode 为transcode/transcode.xml文件中的Root/Transcode/Decode/Video/Overlays/Overlay/Index元素的值
- Destination 代表这个模块源代码中的encodeSource=false
带有最大数值的decode及destination被放置在最顶层,数值为0的encode及source被放置在最底层。
如果在transcode/transcode.xml中overlayIndex的值与这个模块中overlayIndex的值相等并且两个都是encode/source或者都是decode/destination,那么在transcode/transcode.xml中定义的遮盖图层将不会显示。
basePath
basePath 变量用于存储图片文件所在的内容目录的完整路径名.
-
修改类中的onAppStart 方法.
public void onAppStart(IApplicationInstance appInstance)
{
String fullname = appInstance.getApplication().getName() + "/" + appInstance.getName();
getLogger().info("onAppStart: " + fullname);
this.appInstance = appInstance;
String artworkPath = "${com.wowza.wms.context.VHostConfigHome}/content/" + appInstance.getApplication().getName();
Map<String, String> envMap = new HashMap<String, String>();
if (appInstance.getVHost() != null)
{
envMap.put("com.wowza.wms.context.VHost", appInstance.getVHost().getName());
envMap.put("com.wowza.wms.context.VHostConfigHome", appInstance.getVHost().getHomePath());
}
envMap.put("com.wowza.wms.context.Application", appInstance.getApplication().getName());
if (this != null)
envMap.put("com.wowza.wms.context.ApplicationInstance", appInstance.getName());
this.basePath = SystemUtils.expandEnvironmentVariables(artworkPath, envMap);
this.basePath = this.basePath.replace("\\", "/");
if (!this.basePath.endsWith("/"))
this.basePath = this.basePath+"/";
this.appInstance.addLiveStreamTranscoderListener(new TranscoderCreateNotifierExample());
}
-
创建一个子类 EncoderInfo 。这个子类用于为每一个Transcoder的编码输出绑定输入流和输出流的信息。
class EncoderInfo
{
public String encodeName;
public TranscoderSessionVideoEncode sessionVideoEncode = null;
public TranscoderStreamDestinationVideo destinationVideo = null;
public int[] videoPadding = new int[4];
public EncoderInfo(String name, TranscoderSessionVideoEncode sessionVideoEncode, TranscoderStreamDestinationVideo destinationVideo)
{
this.encodeName = name;
this.sessionVideoEncode = sessionVideoEncode;
this.destinationVideo = destinationVideo;
}
}
-
创建一个子类TranscoderCreateNotifierExample
- 创建子类
class TranscoderCreateNotifierExample implements ILiveStreamTranscoderNotify
{
}
-
添加方法
@Override
public void onLiveStreamTranscoderCreate(ILiveStreamTranscoder liveStreamTranscoder, IMediaStream stream)
{
}
@Override
public void onLiveStreamTranscoderDestroy(ILiveStreamTranscoder arg0, IMediaStream arg1)
{
}
@Override
public void onLiveStreamTranscoderInit(ILiveStreamTranscoder arg0, IMediaStream arg1)
{
}
修改onLiveStreamTranscoderCreate 方法.
public void onLiveStreamTranscoderCreate (ILiveStreamTranscoder liveStreamTranscoder, IMediaStream stream)
{
getLogger().info("ModuleTranscoderOverlayExample#TranscoderCreateNotifierExample.onLiveStreamTranscoderCreate["+appInstance.getContextStr()+"]: "+stream.getName());
((LiveStreamTranscoder)liveStreamTranscoder).addActionListener(new TranscoderActionNotifierExample());
}
创建子类TranscoderActionNotifierExample
-
Create the class.
class TranscoderActionNotifierExample extends LiveStreamTranscoderActionNotifyBase
{
TranscoderVideoDecoderNotifyExample transcoder=null;
}
-
创建onSessionVideoEncodeSetup 方法。这里将创建TranscoderVideoDecoderNotifyExample 类, 它继承自TranscoderVideoDecoderNotifier 类. 这个类将对每一个经过系统的帧图像调用它的onBeforeScaleFrame方法
public void onSessionVideoEncodeSetup(LiveStreamTranscoder liveStreamTranscoder, TranscoderSessionVideoEncode sessionVideoEncode)
{
getLogger().info("ModuleTranscoderOverlayExample#TranscoderActionNotifierExample.onSessionVideoEncodeSetup["+appInstance.getContextStr()+"]");
TranscoderStream transcoderStream = liveStreamTranscoder.getTranscodingStream();
if (transcoderStream != null && transcoder==null)
{
TranscoderSession transcoderSession = liveStreamTranscoder.getTranscodingSession();
TranscoderSessionVideo transcoderVideoSession = transcoderSession.getSessionVideo();
List<TranscoderStreamDestination> alltrans = transcoderStream.getDestinations();
int w = transcoderVideoSession.getDecoderWidth();
int h = transcoderVideoSession.getDecoderHeight();
transcoder = new TranscoderVideoDecoderNotifyExample(w,h);
transcoderVideoSession.addFrameListener(transcoder);
//apply an overlay to all outputs
for(TranscoderStreamDestination destination:alltrans)
{
//TranscoderSessionVideoEncode sessionVideoEncode = transcoderVideoSession.getEncode(destination.getName());
TranscoderStreamDestinationVideo videoDestination = destination.getVideo();
System.out.println("sessionVideoEncode:"+sessionVideoEncode);
System.out.println("videoDestination:"+videoDestination);
if (sessionVideoEncode != null && videoDestination !=null)
{
transcoder.addEncoder(destination.getName(),sessionVideoEncode,videoDestination);
}
}
}
return;
}
创建子类TranscoderVideoDecoderNotifyExample
-
创建类.
class TranscoderVideoDecoderNotifyExample extends TranscoderVideoDecoderNotifyBase
{
}
-
增加成员变量
private OverlayImage mainImage=null;private OverlayImage wowzaImage=null;
private OverlayImage wowzaText = null;
private OverlayImage wowzaTextShadow = null;
List<EncoderInfo> encoderInfoList = new ArrayList<EncoderInfo>();
AnimationEvents videoBottomPadding = new AnimationEvents();
-
增加TranscoderVideoDecoderNotifyExample 构造器
public TranscoderVideoDecoderNotifyExample (int srcWidth, int srcHeight)
{
int lowerThirdHeight = 70;
}
-
增加addEncoder 方法
public void addEncoder(String name, TranscoderSessionVideoEncode sessionVideoEncode, TranscoderStreamDestinationVideo destinationVideo)
{
encoderInfoList.add(new EncoderInfo(name, sessionVideoEncode,destinationVideo));
}
-
增加onBeforeScaleFrame 方法。这个方法在每一个帧被注入到Transcoder插件时被调用。你可以在这里为输入流或每一个输出流添加一个图片
public void onBeforeScaleFrame(TranscoderSessionVideo sessionVideo, TranscoderStreamSourceVideo sourceVideo, long frameCount)
{
boolean encodeSource=false;
boolean showTime=false;
double scalingFactor=1.0;
synchronized(lock)
{
if (mainImage != null)
{
//does not need to be done for a static graphic, but left here to build on (transparency/animation)
videoBottomPadding.step();
mainImage.step();
int sourceHeight = sessionVideo.getDecoderHeight();
int sourceWidth = sessionVideo.getDecoderWidth();
if(showTime)
{
Date dNow = new Date( );
SimpleDateFormat ft = new SimpleDateFormat("hh:mm:ss");
wowzaText.SetText(ft.format(dNow));
wowzaTextShadow.SetText(ft.format(dNow));
}
if(encodeSource)
{
//put the image onto the source
scalingFactor = 1.0;
TranscoderVideoOverlayFrame overlay = new TranscoderVideoOverlayFrame(mainImage.GetWidth(scalingFactor),
mainImage.GetHeight(scalingFactor), mainImage.GetBuffer(scalingFactor));
overlay.setDstX(mainImage.GetxPos(scalingFactor));
overlay.setDstY(mainImage.GetyPos(scalingFactor));
sourceVideo.addOverlay(overlayIndex, overlay);
}
else
{
///put the image onto each destination but scaled to fit
for(EncoderInfo encoderInfo: encoderInfoList)
{
int destinationHeight = encoderInfo.destinationVideo.getFrameSizeHeight();
scalingFactor = (double)destinationHeight/(double)sourceHeight;
TranscoderVideoOverlayFrame overlay = new TranscoderVideoOverlayFrame(mainImage.GetWidth(scalingFactor),
mainImage.GetHeight(scalingFactor), mainImage.GetBuffer(scalingFactor));
overlay.setDstX(mainImage.GetxPos(scalingFactor));
overlay.setDstY(mainImage.GetyPos(scalingFactor));
encoderInfo.destinationVideo.addOverlay(overlayIndex, overlay);
//Add padding to the destination video i.e. pinch
encoderInfo.videoPadding[0] = 0; // left
encoderInfo.videoPadding[1] = 0; // top
encoderInfo.videoPadding[2] = 0; // right
encoderInfo.videoPadding[3] = (int)(((double)videoBottomPadding.getStepValue())*scalingFactor); // bottom
encoderInfo.destinationVideo.setPadding(encoderInfo.videoPadding);
}
}
}
}
return;
}
encodeSource
在这个模块中,你可以设置encodeSource 变量来为输入流在编码输出多个码流之前添加一个遮盖图层(encodeSource=true)或者为每一个输出码流添加遮盖图层(encodeSource=false)。这两种方法各有优缺点。
Source (encodeSource=true):
- Benefits: 只用在这里添加一次,所有输出流都会带上这个遮盖图层
- Drawbacks: 图片可能会被再次拉伸而不会向你期望的那样显示,同时,字体也将不会被重画,而是向图片一样被拉伸.
如果视频将被缩小("扭曲")你可以将图片放置在原始视频窗口之外。例如,视频的原始尺寸是640 x 480,然后被扭曲为640 x 400 以为电视观看时在底部留出空白来显示图片
Destination (encodeSource=false):
- Benefits: 如果需要的话,你可以为每一个编码输出设置一个图片。并且字形可以适配输出流的帧图像大小绘制。同时原始视频可以被扭曲,图片也绘制在视频窗口的外面
- Drawbacks: 每一个编码输出都要进行一次图片覆盖的工作(720p, 360p, etc.)
showTime
在onBeforeScaleFrame中设置showTime变量,将会把当前系统时间作为一个文本传递给wowzaText。在这里例子中,这个变量被用于用系统时间的文本来作为遮盖图层来对视频进行修饰。
scalingFactor
这个倍数,代表了输出流帧图像与输入流帧图像的关联关系。如果这个值为.5 意味着输出流帧图像的大小输入流帧图像大小的一半;如果这个值为2.0意味着输出流的帧图像大小是输入流帧图像大小的2倍。
当你完成这些步骤,项目就可以编译了、部署和运行了。然而这时你还不能在视频上看到图片。
在视频上创建一个图片
-
在TranscoderVideoDecoderNotifyExample构造器的最后添加下面的代码来给视频添加一个logo图片(logo_1.png)。
//create a transparent container for the bottom third of the screen.
mainImage = new OverlayImage(0,srcHeight-lowerThirdHeight,srcWidth,lowerThirdHeight,100);
//Create the Wowza logo image
wowzaImage = new OverlayImage(basePath+graphicName,100);
mainImage.addOverlayImage(wowzaImage,srcWidth-wowzaImage.GetWidth(1.0),0);
mainImage 是所有动画的基础容器。它为 It represents a transparent lower 3rd for all other images and text.
wowzaImage是画在mainImage上面的logo图片,它从它的x,y坐标开始。
当在
[install-dir]/examples/LiveVideoStreaming/FlashHTTPPlayer/player.html中观看
myStream_360p和其它所有输出流时,Wowza logo 将显示在屏幕右下角。
在视频上创建一段文本
-
在图片底部放置一段文本
//Add Text with a drop shadow
wowzaText = new OverlayImage("Wowza", 12, "SansSerif", Font.BOLD, Color.white, 66,15,100);
wowzaTextShadow = new OverlayImage("Wowza", 12, "SansSerif", Font.BOLD, Color.darkGray, 66,15,100);
mainImage.addOverlayImage(wowzaText, wowzaImage.GetxPos(1.0)+12, 54);
wowzaText.addOverlayImage(wowzaTextShadow, 1, 1);
这将在OverlayImage的底部一端文本"Wowza"。
当在
[install-dir/examples/LiveVideoStreaming/FlashHTTPPlayer/player.html里观看
myStream_360p 和其它所有输出流时,Wowza logo 和文本将显示在屏幕的右下角。
图片和文本的渐入渐出
要实现图片和文本的渐入渐出,
OverlayImage 类有一个
addFadingStep 方法可以用来实现这个功能。
//Fade the Logo and text independently
wowzaImage.addFadingStep(100,0,25);
wowzaImage.addFadingStep(0,100,25);
wowzaText.addFadingStep(0,100,25);
wowzaText.addFadingStep(100,0,25);
addFadingStep方法接收的参数(<start value>,<end value>,<number of steps>).
上面的例子代码让图片的透明度从
0到
100逐渐增加4个透明度((end-start)/steps) 或 ((100-0)/25) ,然后再反向进行(透明度从
100 到
0)。 如果设置为addFadingStep(0,100,200),那么图片的每个显示步骤增加0.5个透明度((100-0)/200)) ,这样显示的时间会更长。
在上面的例子代码中, 当在
[install-dir]/examples/LiveVideoStreaming/FlashHTTPPlayer/player.html中观看
myStream_360p 和其它流时,Wowza 的logo 和文字会以渐入渐出的方式显示在屏幕右下角。
注意: 下面的代码可以创建一段后面会介绍的动画效果。
图片和文本的渐入渐出
图片旋转
要让图片有动画效果,
OverlayImage 类有一个
addImageStep 方法可以对图像进行更新以实现动画。下面的代码将会把图片旋转4次。
//Rotate the image while fading
wowzaImage.addImageStep(1,37,25);
wowzaImage.addImageStep(1,37,25);
wowzaImage.addImageStep(1,37,25);
wowzaImage.addImageStep(1,37,25);
addImageStep 方法的参数和
addFadingStep 方法的参数一样,包括开始/结束/步骤数量(start/end/step)三个参数。
这个类会把
${com.wowza.wms.plugin.transcoderoverlays.overlayimage.step} 的值设置到当前的step中。
当我们定义一个图片(variable graphicName)时,我们用这个变量。每一个图像会旋转5度,以实现一个旋转效果。
上面的代码例子,会让图像旋转4次(每次25个步骤),整个过程共100个步骤。
文本的动画效果
要让文本实现动画效果,
OverlayImage类有一个
addMovementStep 方法可以更新文本的x轴坐标。
//Animate the text off screen to original location
wowzaText.addMovementStep(-75, 0, wowzaText.GetxPos(1.0), 54, 100);
addMovementStep让文本从x1,y1坐标移动到x2,y2坐标。要移动文本,只要设置步骤和文本的X、y坐标即可。
要完成动画,添加下面的代码
//hold everything for a bit
mainImage.addFadingStep(50);
wowzaImage.addImageStep(50);
wowzaText.addMovementStep(50);
//Fade out
mainImage.addFadingStep(100,0,50);
wowzaImage.addImageStep(50);
wowzaText.addMovementStep(50);
当动画完成时,它将保持50步然后渐出。
视频的扭曲效果
当正在编码一个输出流时,我们可以对源视频进行缩小和扭曲.
//Pinch back video
videoBottomPadding.addAnimationStep(0, 60, 50);
videoBottomPadding.addAnimationStep(100);
//unpinch the video
videoBottomPadding.addAnimationStep(60, 0, 50);
mainImage.addFadingStep(50);
wowzaImage.addImageStep(50);
wowzaText.addMovementStep(50);
videoBottomPadding.addAnimationStep(100);
故障排查
- 使用遮盖图层会给服务器带来压力,因为复杂的图片处理会占用CPU资源,这将使得Transcoder插件跳过一些帧并让视频延时。因此你的服务器必须经过适当的调优以处理大量视频源是很重要的。要了解更多,请阅读Wowza服务器的性能调优.
- 建议你把遮盖图层做小一些。如果帧率为30fps但要花掉超过1/30秒的时间去处理遮盖图层,那么Transcoder可能会跳过某些帧
- 这个特定支持对图片和文本操作以实现动画效果,它不支持类似画中画(PiP)那样对视频流的操作或多个流的组合
- 使用Transcoder 插件是无法在点播(VOD)视频流上添加图片或文字的
- 在转码模版文件的Overlays属性中设置的静态遮盖图片将被用这个特性创建的动态遮盖图片所覆盖
- 你在Wowza Media Server里创建的隐藏字幕(cc)将显示在用这个特性创建的动态遮盖图层的上面