如何在直播流上添加图片和文字?
转载请注明:文章来自www.wowza.cn
注意: 要访问最新的技术资料,请访问http://www.ttstream.com/wowza/
这篇文章向开发者介绍了如何使用Wowza Transcoder 插件在直播流上放置一个图片。它介绍了创建自定义转码模块、在视频上添加图片和文字、以及实现渐进渐出和动画效果的详细过程。 下面的这些例子代码对开发者来说只是一个开始。通过对这些代码进行扩展,可以实现更多高级功能。www.wowza.cn

注意:针对Wowza Media Server® 3.5.0 及更高版本。

内容


开始

概述

准备工作

创建一个新的转码模块

创建一个Transcoder 插件模块

在视频上创建一个图片

在视频上创建一段文字

图片和文字的渐进渐出

旋转图片

文本的动画效果

对视频进行扭曲

故障排查


开始



概述


Wowza Transcoder 插件可以为它所创建的每一个帧所调用,并允许开发者用一个图片和文字对这一帧的图像进行修改。 因为Transcoder AddOn会被重复地调用,因此开发者也可以用它在输出流中创建动画效果。 www.wowza.cn

注意: 你可以从这里下载本文的这些例子代码下载 TranscoderOverlayExampleFiles.zip.

准备工作


This feature requires advanced technical skills. This article is intended for developers who are familiar with Wowza Media Server and are experienced with Java programming and XML.

建议你首先建立一个能够正常播出的直播流,请参照如下进行:

  1. 首先,很重要的一点是你最好将服务器进行合适的调优,请阅读性能调优.

  2. 根据如何为一个直播流配置转码 一步一步进行配置。在创建图层覆盖的Transcoder自定义模块之前,请先对直播流的转码和播放进行测试。请阅读故障排查测试中的Test #1 和Test #2 部分。本文的例子中使用的应用为live 输入流为的stream name 为myStream.

  3. 将这个例子中的内容文件都拷贝到你的Wowza Media Server的content目录下.

    下载TranscoderOverlayExampleFiles.zip后并解压缩,将里面的content目录下的所有文件拷贝到你的Wowza Media Server 安装路径的content目录下([install-dir]/content).

    注意: 如果你要使用自己的图片文件,请确认要符合Transcoder 对遮盖图片的要求

创建一个新的Transcoder模块


要在直播流上防止一个图片或文字,你必须创建一个Transcoder模块。开发下面的例子时,使用了Eclipse Integrated Development Environment (IDE) 和 Eclipse Wowza 插件。要了解更多,请下载Wowza IDE User's Guide.

  1. 创建一个新的项目

    1. 使用Eclipse IDE 创建一个新的Wowza Media Server Project:
      • Project name: ModuleTranscoderOverlayExample
      • Wowza Media Server location: 选择Wowza Media Server的安装目录.
      • Package: com.wowza.wms.plugin.transcoderoverlays
      • Name: ModuleTranscoderOverlayExample

    2. Run > Run Configurations下面,选择ModuleTranscoderOverlayExample.

    3. Arguments tab页, 添加以下VM 参数:
      -Dcom.wowza.wms.native.base="win"

  2. 在配置文件中进行配置

    [install-dir]/conf/live/Application.xml中添加下面的代码:
    Code:
    <Module>
           <Name>ModuleTranscoderOverlayExample</Name>
           <Description>Example Overlay</Description>
           <Class>com.wowza.wms.plugin.transcoderoverlays.ModuleTranscoderOverlayExample</Class>
    </Module>
  3. 启动Wowza Media Server。


创建Transcoder 插件模块


overlayExample 使用下面的Wowza基类和接口在视频上放置一个图片:

  • IliveStreamTranscoderNotify
  • LiveStreamTranscoderActionNotifyBase
  • TranscoderVideoDecoderNotifyBase


下面的例子也包括了OverlayImageAnimationEvents 两个类以实现快速的绘画和动画效果。你必须将这两个classes加到项目中.

  1. 修改 ModuleTranscoderOverlayExample 类.

    1. 导入下面的类包:
      Code:
      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;
      www.wowza.cn
    2. 为这个类添加下面的成员变量.
      Code:
      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中定义的遮盖图片,如何画出这个模块中定义的遮盖图片,遮盖图片的叠加顺序(垂直方向)如下:

      1. 较低数值的Encode及Source
      2. 较高数值的Encode及Source
      3. 较低数值Decode及Destination
      4. 较高数值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.xmloverlayIndex的值与这个模块中overlayIndex的值相等并且两个都是encode/source或者都是decode/destination,那么在transcode/transcode.xml中定义的遮盖图层将不会显示。

      basePath

      basePath 变量用于存储图片文件所在的内容目录的完整路径名.

    3. 修改类中的onAppStart 方法.
      Code:
      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());
      }

  2. 创建一个子类 EncoderInfo 。这个子类用于为每一个Transcoder的编码输出绑定输入流和输出流的信息。
    Code:
    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;
        }
    }
  3. 创建一个子类TranscoderCreateNotifierExample

    1. 创建子类
      Code:
      class TranscoderCreateNotifierExample implements ILiveStreamTranscoderNotify
      {
      }
    2. 添加方法
      Code:
      @Override
      public void onLiveStreamTranscoderCreate(ILiveStreamTranscoder liveStreamTranscoder, IMediaStream stream) 
      {
      }
       
      @Override
      public void onLiveStreamTranscoderDestroy(ILiveStreamTranscoder arg0, IMediaStream arg1) 
      {
      }
       
      @Override
      public void onLiveStreamTranscoderInit(ILiveStreamTranscoder arg0, IMediaStream arg1) 
      {
      }
    3. 修改onLiveStreamTranscoderCreate 方法.
      Code:
      public void onLiveStreamTranscoderCreate (ILiveStreamTranscoder liveStreamTranscoder, IMediaStream stream)
      {
            getLogger().info("ModuleTranscoderOverlayExample#TranscoderCreateNotifierExample.onLiveStreamTranscoderCreate["+appInstance.getContextStr()+"]: "+stream.getName());
            ((LiveStreamTranscoder)liveStreamTranscoder).addActionListener(new TranscoderActionNotifierExample());
      }

  4. 创建子类TranscoderActionNotifierExample

    1. Create the class.
      Code:
      class TranscoderActionNotifierExample extends LiveStreamTranscoderActionNotifyBase
      {
            TranscoderVideoDecoderNotifyExample transcoder=null;
      }
    2. 创建onSessionVideoEncodeSetup 方法。这里将创建TranscoderVideoDecoderNotifyExample 类, 它继承自TranscoderVideoDecoderNotifier 类. 这个类将对每一个经过系统的帧图像调用它的onBeforeScaleFrame方法
      Code:
      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;
      }

  5. 创建子类TranscoderVideoDecoderNotifyExample

    1. 创建类.
      Code:
      class TranscoderVideoDecoderNotifyExample extends TranscoderVideoDecoderNotifyBase
      {
      }
    2. 增加成员变量
      Code:
      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();
    3. 增加TranscoderVideoDecoderNotifyExample 构造器
      Code:
      public TranscoderVideoDecoderNotifyExample (int srcWidth, int srcHeight)
      {
      int lowerThirdHeight = 70;
      }
    4. 增加addEncoder 方法
      Code:
      public void addEncoder(String name, TranscoderSessionVideoEncode sessionVideoEncode, TranscoderStreamDestinationVideo destinationVideo)
      {
          encoderInfoList.add(new EncoderInfo(name, sessionVideoEncode,destinationVideo));
      }
    5. 增加onBeforeScaleFrame 方法。这个方法在每一个帧被注入到Transcoder插件时被调用。你可以在这里为输入流或每一个输出流添加一个图片
      Code:
      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)。
    Code:
    //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 将显示在屏幕右下角。

在视频上创建一段文本


  • 在图片底部放置一段文本
    Code:
    //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 方法可以用来实现这个功能。
Code:
//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>).

上面的例子代码让图片的透明度从0100逐渐增加4个透明度((end-start)/steps) 或 ((100-0)/25) ,然后再反向进行(透明度从1000)。 如果设置为addFadingStep(0,100,200),那么图片的每个显示步骤增加0.5个透明度((100-0)/200)) ,这样显示的时间会更长。

在上面的例子代码中, 当在[install-dir]/examples/LiveVideoStreaming/FlashHTTPPlayer/player.html中观看myStream_360p 和其它流时,Wowza 的logo 和文字会以渐入渐出的方式显示在屏幕右下角。

注意: 下面的代码可以创建一段后面会介绍的动画效果。
图片和文本的渐入渐出

  • 删除或屏蔽掉上面的代码,用下面的代码替换。
    Code:
    //do nothing for a bit
    mainImage.addFadingStep(50);
    wowzaImage.addImageStep(50);
    wowzaText.addMovementStep(50);
     
    //Fade the logo and text
    mainImage.addFadingStep(0,100,100);
    这块代码将重载addFadingStep方法,它只设置了step value然后不做任何动作。


图片旋转


要让图片有动画效果,OverlayImage 类有一个 addImageStep 方法可以对图像进行更新以实现动画。下面的代码将会把图片旋转4次。
Code:
//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轴坐标。
Code:
//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坐标即可。

要完成动画,添加下面的代码
Code:
//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步然后渐出。

视频的扭曲效果


当正在编码一个输出流时,我们可以对源视频进行缩小和扭曲.
Code:
//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插件跳过一些帧并让视频延时。因此你的服务器必须经过适当的调优以处理大量视频源是很重要的。要了解更多,请阅读性能调优.

  • 建议你把遮盖图层做小一些。如果帧率为30fps但要花掉超过1/30秒的时间去处理遮盖图层,那么Transcoder可能会跳过某些帧

  • 这个特定支持对图片和文本操作以实现动画效果,它不支持类似画中画(PiP)那样对视频流的操作或多个流的组合

  • 使用Transcoder 插件是无法在点播(VOD)视频流上添加图片或文字的

  • 在转码模版文件的Overlays属性中设置的静态遮盖图片将被用这个特性创建的动态遮盖图片所覆盖

  • 你在Wowza Media Server里创建的隐藏字幕(cc)将显示在用这个特性创建的动态遮盖图层的上面