如何使用Wowza nDVR的 playlist request api?
这篇文章演示了如何使用Wowza nDVR API来控制playlist requests。
默认的特性
默认情况下,当nDVR收到一个DVR HLS playlist请求时,它将确定一个播放列表供播放器播放。
默认的逻辑如下:
- 如果一个输入流正在被录制,它将被看作是live, 否则它将被看作recorded。
-
live和recorded 都使用了一个开始时间:
- 录制的最早的时间,或者
- 最近的时间减去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)的时长。
要使用这个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;
}