多协议、性能稳定、丰富API的流媒体服务器软件
如何使用Wowza nDVR的 playlist request api?
这篇文章演示了如何使用Wowza nDVR API来控制playlist requests。

默认的特性

默认情况下,当nDVR收到一个DVR HLS playlist请求时,它将确定一个播放列表供播放器播放。

  • HLS:
    http://[wowza-address]:1935/dvr/myStream/playlist.m3u8?DVR
    			

默认的逻辑如下:

  • 如果一个输入流正在被录制,它将被看作是live, 否则它将被看作recorded
  • liverecorded 都使用了一个开始时间:
    • 录制的最早的时间,或者
    • 最近的时间减去DVR的平移窗口时间(如果你设置了它)。
  • live流没有结束时间。
  • recorded流的播放在录制截至的时间停止。

由于http直播流的天然特性,live流必须缓存一些在当前直播点之后的额外的音视频数据片段(由于要先进行切片)。了解更多Wowza产品细节 Recorded 流没有这个限制,只是在他们的playlist中保存缓存的音视频数据片段。

当DVR store是Live状态或Recored状态时,Wowza返回给播放器的playlist/manifest是不同的,不同的播放技术在处理这些playlist时也是不同的。 例如,live状态的playlist通常从当前直播点"live point"开始。而recorded状态的playlists会从最早的录制点开始。

Playlist Request Delegate

nDVR 提供了一个可以配置delegate的属性"dvrPlaylistRequestDelegate",它是通过java提供了一个不同的playlist request的机制。

这个属性应该被设置在Application.xml文件的Application/DVR/Properties里面,应该指向一个有效的继承了"com.wowza.wms.dvr.DvrBasePlaylistRequestDelegate"的classname。

当播放列表被请求时,这个delegate的方法getDvrPlaylistRequest()将会被调用来提供playlist request。
Playlist Request

Playlist Request Delegate 的职责是生成一个playlist请求对象。 它传递一个application context、一个DVR store object (它包含了一个查询潜在的切片数据的方法)、 一个通过URL参数传递进来的queryMap。

一个播放列表请求有一个开始时间和一个可选的结束时间。

如果设置的开始时间是无效的(例如,在录制开始前的时间或者录制结束后的时间或录制结束的时间),那么默认将使用录制的开始时间。

如果没有设置结束时间:

  • 对于直播流内容(正在被录制,还没有结束),返回给播放器的playlist将会是直播播放列表。
  • 对于录制完成的内容(当前没有在进行录制),返回给播放器的playlist将会是录制内容的播放列表,它采用录制结束时时间。


如果为一个直播流内容设置了结束时间,那么最后的播放列表将是recorded 变量,因为直播流没有结束时间。

一个例子

Wowza nDVR包含了一个 playlist request delegate: "com.wowza.wms.dvr.impl.DvrStartDurationPlaylistRequestDelegate". 这个delegate 根据URL中的查询参数生成了playlist request.了解更多Wowza产品细节 例如,从第一分钟到第六分钟6, 我们设置设置了一个开始时间60 seconds (60000 ms)和一个300 seconds (300000 ms)的时长。

  • HLS:
    http://[wowza-address]:1935/dvr/myStream/playlist.m3u8?DVR&wowzadvrplayliststart=60000&wowzadvrplaylistduration=300000
    			


要使用这个delegate, 在Application.xml 文件的 Application/DVR/Properties中增加下面的属性:
<Properties>
	<Property>
		<Name>dvrPlaylistRequestDelegate</Name>
		<Value>com.wowza.wms.dvr.impl.DvrStartDurationPlaylistRequestDelegate</Value>
	</Property>	
</Properties>
要使用不同的查询参数,在同样的位置添加下面的属性,改变它的值即可:
<Properties>
	<Property>
		<Name>dvrPlaylistRequestDelegate</Name>
		<Value>com.wowza.wms.dvr.impl.DvrStartDurationPlaylistRequestDelegate</Value>
	</Property>
	<Property>
		<Name>dvrPlaylistDurationQueryParameter</Name>
		<Value>wowzadvrplaylistduration</Value>
	</Property>	
	<Property>
		<Name>dvrPlaylistStartQueryParameter</Name>
		<Value>wowzadvrplayliststart</Value>
	</Property>	
</Properties>
		
为这些Delegate添加调试日志,在Application.xml文件的 Application/DVR/Properties中添加下面的属性:
<Properties>
	<Property>
		<Name>dvrDebugPlaylistRequest</Name>
		<Value>true</Value>
                 <Type>Boolean</Type>
	</Property>	
</Properties>
		
Delegate示例

如果要提供一个你自己的playlist delegate,它和"DvrStartDurationPlaylistRequestDelegate"基本类似。 你必须提供2个public方法以及确定playlist的逻辑。

这个方法被一个单码率播放列表请求调用:
public DvrPlaylistRequest getDvrPlaylistRequest(IHTTPStreamerApplicationContext appContext, IDvrStreamStore store, Map<String, String> queryMap)
		
这个方法被一个多码率自适应播放列表请求调用:
public DvrPlaylistRequest getDvrPlaylistRequest(IHTTPStreamerApplicationContext appContext, List<IDvrStreamStore> stores, Map<String, String> queryMap)
		



package com.example.dvr.impl;

import java.util.*;

import com.wowza.wms.application.WMSProperties;
import com.wowza.wms.dvr.*;
import com.wowza.wms.dvr.IDvrConstants.DvrTimeScale;
import com.wowza.wms.httpstreamer.model.IHTTPStreamerApplicationContext;
import com.wowza.wms.logging.WMSLoggerFactory;

public class DvrStartDurationPlaylistRequestDelegate extends DvrBasePlaylistRequestDelegate {
	private static final String CLASSNAME = "DvrStartDurationPlaylistRequestDelegate";
	private static final Class<DvrStartDurationPlaylistRequestDelegate> CLASS = DvrStartDurationPlaylistRequestDelegate.class;
    
    public static final String DVR_QUERYSTR_PLAYLIST_DURATION = "wowzadvrplaylistduration";
    public static final String DVR_QUERYSTR_PLAYLIST_START = "wowzadvrplayliststart";
    public static final String PROPKEY_DVR_PLAYLIST_DURATION_QUERY_PARAMETER = "dvrPlaylistDurationQueryParameter";
    public static final String PROPKEY_DVR_PLAYLIST_START_QUERY_PARAMETER = "dvrPlaylistStartQueryParameter";
    public static final String PROPKEY_DVR_PLAYLIST_LOG_REQUESTS = "dvrPlaylistDebugRequests";
    
    private boolean doDebug = false;

    
    public DvrPlaylistRequest getDvrPlaylistRequest(IHTTPStreamerApplicationContext appContext, IDvrStreamStore store, Map<String, String> queryMap) {       
        DvrPlaylistRequest availablePlaylist = getDefaultPlaylistRequest(DvrTimeScale.DVR_TIME, store);
        
        DvrPlaylistRequest newRequest = createRequestFromQueryParams(appContext, queryMap, availablePlaylist);
        
        return newRequest;
    }


    public DvrPlaylistRequest getDvrPlaylistRequest(IHTTPStreamerApplicationContext appContext, 
                                                                        List<IDvrStreamStore> stores, Map<String, String> queryMap) {
        DvrPlaylistRequest availablePlaylist = getDefaultPlaylistRequest(DvrTimeScale.DVR_TIME, stores);

        DvrPlaylistRequest newRequest = createRequestFromQueryParams(appContext, queryMap, availablePlaylist);

        return newRequest;
    }

    private DvrPlaylistRequest createRequestFromQueryParams(IHTTPStreamerApplicationContext appContext, 
                                                                                          Map<String, String> queryMap, DvrPlaylistRequest availablePlaylist) {
        DvrPlaylistRequest newRequest = new DvrPlaylistRequest();
        if (availablePlaylist != null) {
            newRequest.setPlaylistEnd(availablePlaylist.getPlaylistEnd());
            newRequest.setPlaylistStart(availablePlaylist.getPlaylistStart());
        }
        
        WMSProperties dvrProperties = getDvrProperties(appContext);
        String playStartQueryParameter = dvrProperties.getPropertyStr(PROPKEY_DVR_PLAYLIST_START_QUERY_PARAMETER, DVR_QUERYSTR_PLAYLIST_START);
        String playDurationQueryParameter = dvrProperties.getPropertyStr(PROPKEY_DVR_PLAYLIST_DURATION_QUERY_PARAMETER, DVR_QUERYSTR_PLAYLIST_DURATION);

	this.doDebug = dvrProperties.getPropertyBoolean(PROPKEY_DVR_PLAYLIST_LOG_REQUESTS, doDebug);


        String playStartStr = queryMap.get(playStartQueryParameter);
        String playDurationStr = queryMap.get(playDurationQueryParameter);

        if (doDebug) {
            WMSLoggerFactory.getLogger(CLASS).info(String.format("%s : Request: %s:%s %s:%s ", CLASSNAME, playStartQueryParameter, playStartStr, playDurationQueryParameter, playDurationStr)); 
            WMSLoggerFactory.getLogger(CLASS).info(String.format("%s : Available Playlist: %s ", CLASSNAME, availablePlaylist));   	
        }

        if (availablePlaylist == null) {
            if (doDebug) {
                WMSLoggerFactory.getLogger(CLASS).warn(String.format("%s : availablePlaylist is null.", CLASSNAME));    
            }
            return newRequest;
        }

        if (playStartStr != null)
        {
            try
            {
                long playStart = Long.parseLong(playStartStr);
                if (playStart < availablePlaylist.getPlaylistStart()) {
                	if (doDebug) {
                		WMSLoggerFactory.getLogger(CLASS).warn(String.format("%s : requestedStart:%d < availableStart:%d.  Using availableStart.", CLASSNAME, playStart, availablePlaylist.getPlaylistStart()));   	
                	}
                } 
                else if (availablePlaylist.hasSpecifiedEnd() && playStart > availablePlaylist.getPlaylistEnd()) {
                	if (doDebug) {
                		WMSLoggerFactory.getLogger(CLASS).warn(String.format("%s : requestedStart:%d > availableEnd:%d.", CLASSNAME, playStart, availablePlaylist.getPlaylistEnd()));   	
                	}
                } else {
                    newRequest.setPlaylistStart(playStart);
                } 
                
            }
            catch(Exception e)
            {
            }
        }
        


        if (playDurationStr != null)
        {
            try
            {
                long playDuration = Long.parseLong(playDurationStr);
                long playEnd = newRequest.getPlaylistStart() + playDuration;
                if (playEnd < availablePlaylist.getPlaylistStart()) {
                	if (doDebug) {
                		WMSLoggerFactory.getLogger(CLASS).warn(String.format("%s : requestedEnd:%d < availableStart:%d.  Using availableStart.", CLASSNAME, playEnd, availablePlaylist.getPlaylistStart()));   	
                	}
                } else if (availablePlaylist.hasSpecifiedEnd() && playEnd > availablePlaylist.getPlaylistEnd()) {
                	if (doDebug) {
                		WMSLoggerFactory.getLogger(CLASS).warn(String.format("%s : requestedEnd:%d > availableEnd:%d.  Using availableEnd.", CLASSNAME, playEnd, availablePlaylist.getPlaylistEnd()));   	
                	}
                } else {
                    newRequest.setPlaylistEnd(playEnd);
                } 
            }
            catch(Exception e)
            {
            }
        }
        if (doDebug) {
            WMSLoggerFactory.getLogger(CLASS).info(String.format("%s : Resolved Playlist: %s ", CLASSNAME, newRequest));   	
        }

        return newRequest;
    }
}
代码示例: 查询DVR Store 时间。

这个代码片段演示了如何在你的playlist delegate中查询DVR store 来确定可用的时间。 当创建你自己的playlist request delegate时,可能会用上类似的代码。


Time Map 包含了dvr-time、packet time以及utc time 的对应表,every time the time has been reset.

private static final DateFormat formatter = new SimpleDateFormat("yyyy-MM-dd-HH:mm:ss");

public DvrPlaylistRequest getDvrPlaylistRequest(IHTTPStreamerApplicationContext appContext, IDvrStreamStore store, Map<String, String> queryMap) {
    // . . .
        
    // Look at store and determine type (audio or video)
    int type = this.chooseManifestType(store);
        
    IDvrManifest manifest = store.getManifest();

    DvrManifestEntry firstEntry = manifest.getFirstEntry(type);
    System.out.printf("first   : %s\n", formatTime(firstEntry));

    // The last live DVR chunk is earlier than the last recorded for "live" stores
    if (store.isLive()) {
        DvrManifestEntry lastLiveEntry = manifest.getLastLiveEntry(type);
        System.out.printf("lastLive: %s\n", formatTime(lastLiveEntry));
    }
        
    DvrManifestEntry lastRecordedEntry = manifest.getLastRecordedEntry(type);
    System.out.printf("lastRec : %s\n", formatTime(lastRecordedEntry));
        
    IDvrTimeMap timeMap = manifest.getTimeMap();
    Collection<DvrManifestEntry> times = timeMap.getIndexMap().values();
    for (DvrManifestEntry e : times) {
        DvrManifestTimeMapEntry te = (DvrManifestTimeMapEntry)e;
        System.out.printf("timeSpan: %s\n", formatTime(te));
    }
        
    // . . .
}

private String formatTime(DvrManifestEntry entry) {
    if (formatter == null || entry == null) {
        return "format error";
    }
    return String.format("dvrTime:%12d pt:%12d utcTime:%s", 
                                  entry.getStartTimecode(), entry.getPacketStartTime(), formatter.format(new Date(entry.getUtcStartTime())));
}

例子代码:创建基于UTC时间戳的playlist

这个代码片段演示如何创建一个基于UTC时间戳的playlist:

String UTC_FORMAT = "yyyy-MM-dd-HH:mm:ss"
DateFormat formatter = new SimpleDateFormat(UTC_FORMAT);

public DvrPlaylistRequest getDvrPlaylistRequest(IHTTPStreamerApplicationContext appContext, IDvrStreamStore store, Map<String, String> queryMap) {
    // . . . This could come from URL param or some other manner
    String startStr= "2012-02-14-11:30:00";

     // This is entire playlist request in UTC
     DvrPlaylistRequest fullPlaylistRequest = getDefaultLivePlaylistRequest(DvrTimeScale.UTC_TIME, store);

    // Convert start String to UTC
    Date date = null;
    if (!StringUtils.isEmpty(startStr)) {
         try {
            date = (Date)formatter.parse(startStr);  
         } catch (ParseException e) {
            date = null;
            //e.printStackTrace();
        }
    }
    // System.out.printf("'%s' --> date:%s\n", startStr, date);

     // If the date specified is less than the initial date we have to play, its not valid
     if (date != null && date.before(new Date(fullPlaylistRequest.getPlaylistStart())))
     {
          System.out.println("Requested start time before actual recording.");
          date = new Date(fullPlaylistRequest.getPlaylistStart());
     }

    DvrPlaylistRequest req;
    if (date != null) {
        req = new DvrPlaylistRequest(DvrTimeScale.UTC_TIME);
        req.setPlaylistStart(date.getTime());
    } else {
        // Use default
        req = super.getDvrPlaylistRequest(appContext, store, queryMap);
    }
    return req;
}