1
0
Fork 0
mirror of https://github.com/ossrs/srs.git synced 2025-03-09 15:49:59 +00:00

refine srs player for hls, extract a HlsNetStream.

This commit is contained in:
winlin 2016-03-19 21:54:46 +08:00
parent 2492562d25
commit 77167f3331
13 changed files with 862 additions and 949 deletions

BIN
trunk/research/players/srs_player/release/srs_player.swf Executable file → Normal file

Binary file not shown.

View file

@ -1,11 +0,0 @@
package
{
public class Consts
{
// refresh every ts_fragment_seconds*M3u8RefreshRatio
public static var M3u8RefreshRatio:Number = 0.5;
// parse ts every this ms.
public static var TsParseAsyncInterval:Number = 80;
}
}

View file

@ -1,120 +0,0 @@
package
{
import flash.utils.Dictionary;
public class Dict
{
private var _dict:Dictionary;
private var _size:uint;
public function Dict()
{
clear();
}
/**
* get the underlayer dict.
* @remark for core-ng.
*/
public function get dict():Dictionary
{
return _dict;
}
public function has(key:Object):Boolean
{
return (key in _dict);
}
public function get(key:Object):Object
{
return ((key in _dict) ? _dict[key] : null);
}
public function set(key:Object, object:Object):void
{
if (!(key in _dict))
{
_size++;
}
_dict[key] = object;
}
public function remove(key:Object):Object
{
var object:Object;
if (key in _dict)
{
object = _dict[key];
delete _dict[key];
_size--;
}
return (object);
}
public function keys():Array
{
var array:Array = new Array(_size);
var index:int;
for (var key:Object in _dict)
{
var _local6:int = index++;
array[_local6] = key;
}
return (array);
}
public function values():Array
{
var array:Array = new Array(_size);
var index:int;
for each (var value:Object in _dict)
{
var _local6:int = index++;
array[_local6] = value;
};
return (array);
}
public function clear():void
{
_dict = new Dictionary();
_size = 0;
}
public function toArray():Array
{
var array:Array = new Array(_size * 2);
var index:int;
for (var key:Object in _dict)
{
var _local6:int = index++;
array[_local6] = key;
var _local7:int = index++;
array[_local7] = _dict[key];
};
return (array);
}
public function toObject():Object
{
return (toArray());
}
public function fromObject(object:Object):void
{
clear();
var index:uint;
while (index < (object as Array).length) {
set((object as Array)[index], (object as Array)[(index + 1)]);
index += 2;
};
}
public function get size():uint
{
return (_size);
}
}
}

View file

@ -1,97 +0,0 @@
package
{
import flash.utils.ByteArray;
/**
* a piece of flv, fetch from cdn or p2p.
*/
public class FlvPiece
{
private var _pieceId:Number;
protected var _flv:ByteArray;
/**
* the private object for the channel,
* for example, the cdn channel will set to CdnEdge object.
*/
private var _privateObject:Object;
/**
* when encoder error, this piece cannot be generated,
* and it should be skip. default to false.
*/
private var _skip:Boolean;
public function FlvPiece(pieceId:Number)
{
_pieceId = pieceId;
_flv = null;
_skip = false;
}
/**
* when piece is fetch ok.
*/
public function onPieceDone(flv:ByteArray):void
{
// save body.
_flv = flv;
}
/**
* when piece is fetch error.
*/
public function onPieceError():void
{
}
/**
* when piece is empty.
*/
public function onPieceEmpty():void
{
}
/**
* destroy the object, set reference to null.
*/
public function destroy():void
{
_privateObject = null;
_flv = null;
}
public function get privateObject():Object
{
return _privateObject;
}
public function set privateObject(v:Object):void
{
_privateObject = v;
}
public function get skip():Boolean
{
return _skip;
}
public function set skip(v:Boolean):void
{
_skip = v;
}
public function get pieceId():Number
{
return _pieceId;
}
public function get flv():ByteArray
{
return _flv;
}
public function get completed():Boolean
{
return _flv != null;
}
}
}

View file

@ -1,5 +1,264 @@
package package
{ {
import flash.events.Event;
import flash.events.ProgressEvent;
import flash.external.ExternalInterface;
import flash.net.NetConnection;
import flash.net.NetStream;
import flash.net.NetStreamAppendBytesAction;
import flash.net.URLLoader;
import flash.net.URLLoaderDataFormat;
import flash.net.URLRequest;
import flash.net.URLRequestHeader;
import flash.net.URLRequestMethod;
import flash.net.URLStream;
import flash.net.URLVariables;
import flash.utils.ByteArray;
import flash.utils.setTimeout;
// the NetStream to play hls or hls+.
public class HlsNetStream extends NetStream
{
private var hls:HlsCodec = null; // parse m3u8 and ts
// for hls codec.
public var m3u8_refresh_ratio:Number;
public var ts_parse_async_interval:Number;
// play param url.
private var user_url:String = null;
/**
* create stream to play hls.
* @param m3u8_refresh_ratio, for example, 0.5, fetch m3u8 every 0.5*ts_duration.
* @param ts_parse_async_interval, for example, 80ms to parse ts async.
*/
public function HlsNetStream(m3u8_refresh_ratio:Number, ts_parse_async_interval:Number, conn:NetConnection)
{
super(conn);
this.m3u8_refresh_ratio = m3u8_refresh_ratio;
this.ts_parse_async_interval = ts_parse_async_interval;
hls = new HlsCodec(this);
}
/**
* to play the hls stream.
* for example, HlsNetStream.play("http://ossrs.net:8080/live/livestream.m3u8").
* user can set the metadata callback by:
* var ns:NetStream = new HlsNetStream(...);
* ns.client = {};
* ns.client.onMetaData = system_on_metadata;
*/
public override function play(... params):void
{
super.play(null);
user_url = params[0] as String;
refresh_m3u8();
}
/////////////////////////////////////////////////////////////////////////////////////////
////////////////////////////Private Section//////////////////////////////////////////////
/////////////////////////////////////////////////////////////////////////////////////////
private var parsed_ts_seq_no:Number = -1;
private function refresh_m3u8():void {
download(user_url, function(stream:ByteArray):void {
var m3u8:String = stream.toString();
hls.parse(user_url, m3u8);
// redirect by variant m3u8.
if (hls.variant) {
var smu:String = hls.getTsUrl(0);
log("variant hls=" + user_url + ", redirect2=" + smu);
user_url = smu;
setTimeout(refresh_m3u8, 0);
return;
}
// fetch from the last one.
if (parsed_ts_seq_no == -1) {
parsed_ts_seq_no = hls.seq_no + hls.tsCount - 1;
}
// not changed.
if (parsed_ts_seq_no >= hls.seq_no + hls.tsCount) {
refresh_ts();
return;
}
// parse each ts.
var nb_ts:Number = hls.seq_no + hls.tsCount - parsed_ts_seq_no;
log("m3u8 changed, got " + nb_ts + " new ts, count=" + hls.tsCount + ", seqno=" + hls.seq_no + ", parsed=" + parsed_ts_seq_no);
refresh_ts();
})
}
private function refresh_ts():void {
// all ts parsed.
if (parsed_ts_seq_no >= hls.seq_no + hls.tsCount) {
var to:Number = 1000;
if (hls.tsCount > 0) {
to = hls.duration * 1000 / hls.tsCount * m3u8_refresh_ratio;
}
setTimeout(refresh_m3u8, to);
log("m3u8 not changed, retry after " + to.toFixed(2) + "ms");
return;
}
// parse current ts.
var uri:String = hls.getTsUrl(parsed_ts_seq_no - hls.seq_no);
// parse metadata from uri.
if (uri.indexOf("?") >= 0) {
var uv:URLVariables = new URLVariables(uri.substr(uri.indexOf("?") + 1));
var obj:Object = {};
for (var k:String in uv) {
var v:String = uv[k];
if (k == "shp_sip1") {
obj.srs_server_ip = v;
} else if (k == "shp_cid") {
obj.srs_id = v;
} else if (k == "shp_pid") {
obj.srs_pid = v;
}
//log("uv[" + k + "]=" + v);
}
if (client && client.hasOwnProperty("onMetaData")) {
client.onMetaData(obj);
}
}
download(uri, function(stream:ByteArray):void{
log("got ts seqno=" + parsed_ts_seq_no + ", " + stream.length + " bytes");
var flv:FlvPiece = new FlvPiece(parsed_ts_seq_no);
var body:ByteArray = new ByteArray();
stream.position = 0;
hls.parseBodyAsync(flv, stream, body, function():void{
body.position = 0;
//log("ts parsed, seqno=" + parsed_ts_seq_no + ", flv=" + body.length + "B");
onFlvBody(uri, body);
parsed_ts_seq_no++;
setTimeout(refresh_ts, 0);
});
});
}
private function download(uri:String, completed:Function):void {
// http get.
var url:URLStream = new URLStream();
var stream:ByteArray = new ByteArray();
url.addEventListener(ProgressEvent.PROGRESS, function(evt:ProgressEvent):void {
if (url.bytesAvailable <= 0) {
return;
}
//log(uri + " total=" + evt.bytesTotal + ", loaded=" + evt.bytesLoaded + ", available=" + url.bytesAvailable);
var bytes:ByteArray = new ByteArray();
url.readBytes(bytes, 0, url.bytesAvailable);
stream.writeBytes(bytes);
});
url.addEventListener(Event.COMPLETE, function(evt:Event):void {
log(uri + " completed, total=" + stream.length + "bytes");
if (url.bytesAvailable <= 0) {
completed(stream);
return;
}
//log(uri + " completed" + ", available=" + url.bytesAvailable);
var bytes:ByteArray = new ByteArray();
url.readBytes(bytes, 0, url.bytesAvailable);
stream.writeBytes(bytes);
completed(stream);
});
// we set to the query.
uri += ((uri.indexOf("?") == -1)? "?":"&") + "shp_xpsid=" + XPlaybackSessionId;
var r:URLRequest = new URLRequest(uri);
// seems flash not allow set this header.
// @remark disable it for it will cause security exception.
//r.requestHeaders.push(new URLRequestHeader("X-Playback-Session-Id", XPlaybackSessionId));
log("start download " + uri);
url.load(r);
}
// the uuid similar to Safari, to identify this play session.
// @see https://github.com/winlinvip/srs-plus/blob/bms/trunk/src/app/srs_app_http_stream.cpp#L45
public var XPlaybackSessionId:String = createRandomIdentifier(32);
private function createRandomIdentifier(length:uint, radix:uint = 61):String {
var characters:Array = new Array('0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q',
'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h',
'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y',
'z');
var id:Array = new Array();
radix = (radix > 61) ? 61 : radix;
while (length--) {
id.push(characters[randomIntegerWithinRange(0, radix)]);
}
return id.join('');
}
private function randomIntegerWithinRange(min:int, max:int):int {
return Math.floor(Math.random() * (1 + max - min) + min);
}
// callback for hls.
public var flvHeader:ByteArray = null;
public function onSequenceHeader():void {
var s:NetStream = super;
s.appendBytesAction(NetStreamAppendBytesAction.RESET_BEGIN);
s.appendBytes(flvHeader);
log("FLV: sps/pps " + flvHeader.length + " bytes");
writeFlv(flvHeader);
}
private function onFlvBody(uri:String, flv:ByteArray):void {
if (!flvHeader) {
return;
}
var s:NetStream = super;
s.appendBytes(flv);
log("FLV: ts " + uri + " parsed to flv " + flv.length + " bytes");
writeFlv(flv);
}
private function writeFlv(data:ByteArray):void {
return;
var r:URLRequest = new URLRequest("http://192.168.1.117:8088/api/v1/flv");
r.method = URLRequestMethod.POST;
r.data = data;
var pf:URLLoader = new URLLoader();
pf.dataFormat = URLLoaderDataFormat.BINARY;
pf.load(r);
}
private function log(msg:String):void {
msg = "[" + new Date() +"][srs-player] " + msg;
trace(msg);
if (!flash.external.ExternalInterface.available) {
return;
}
ExternalInterface.call("console.log", msg);
}
}
}
import flash.events.Event; import flash.events.Event;
import flash.net.URLLoader; import flash.net.URLLoader;
import flash.net.URLRequest; import flash.net.URLRequest;
@ -9,7 +268,7 @@ package
/** /**
* the hls main class. * the hls main class.
*/ */
public class Hls class HlsCodec
{ {
private var m3u8:M3u8; private var m3u8:M3u8;
@ -25,12 +284,12 @@ package
private var video_sh_tag:ByteArray; private var video_sh_tag:ByteArray;
private var audio_sh_tag:ByteArray; private var audio_sh_tag:ByteArray;
private var owner:M3u8Player; private var owner:HlsNetStream;
private var _log:ILogger = new TraceLogger("HLS"); private var _log:ILogger = new TraceLogger("HLS");
public static const SRS_TS_PACKET_SIZE:int = 188; public static const SRS_TS_PACKET_SIZE:int = 188;
public function Hls(o:M3u8Player) public function HlsCodec(o:HlsNetStream)
{ {
owner = o; owner = o;
m3u8 = new M3u8(this); m3u8 = new M3u8(this);
@ -155,7 +414,7 @@ package
nb_parsed += each_parse; nb_parsed += each_parse;
if (nb_parsed < ts_packets) { if (nb_parsed < ts_packets) {
flash.utils.setTimeout(aysncParse, Consts.TsParseAsyncInterval); flash.utils.setTimeout(aysncParse, owner.ts_parse_async_interval);
return; return;
} }
@ -260,7 +519,7 @@ package
{ {
for (var i:int = 0; (maxTsPackets == -1 || i < maxTsPackets) && data.bytesAvailable > 0; i++) { for (var i:int = 0; (maxTsPackets == -1 || i < maxTsPackets) && data.bytesAvailable > 0; i++) {
var tsBytes:ByteArray = new ByteArray(); var tsBytes:ByteArray = new ByteArray();
data.readBytes(tsBytes, 0, Hls.SRS_TS_PACKET_SIZE); data.readBytes(tsBytes, 0, HlsCodec.SRS_TS_PACKET_SIZE);
context.decode(tsBytes, handler); context.decode(tsBytes, handler);
} }
} }
@ -281,6 +540,326 @@ package
loader.load(url); loader.load(url);
} }
} }
import flash.utils.Dictionary;
class Dict
{
private var _dict:Dictionary;
private var _size:uint;
public function Dict()
{
clear();
}
/**
* get the underlayer dict.
* @remark for core-ng.
*/
public function get dict():Dictionary
{
return _dict;
}
public function has(key:Object):Boolean
{
return (key in _dict);
}
public function get(key:Object):Object
{
return ((key in _dict) ? _dict[key] : null);
}
public function set(key:Object, object:Object):void
{
if (!(key in _dict))
{
_size++;
}
_dict[key] = object;
}
public function remove(key:Object):Object
{
var object:Object;
if (key in _dict)
{
object = _dict[key];
delete _dict[key];
_size--;
}
return (object);
}
public function keys():Array
{
var array:Array = new Array(_size);
var index:int;
for (var key:Object in _dict)
{
var _local6:int = index++;
array[_local6] = key;
}
return (array);
}
public function values():Array
{
var array:Array = new Array(_size);
var index:int;
for each (var value:Object in _dict)
{
var _local6:int = index++;
array[_local6] = value;
};
return (array);
}
public function clear():void
{
_dict = new Dictionary();
_size = 0;
}
public function toArray():Array
{
var array:Array = new Array(_size * 2);
var index:int;
for (var key:Object in _dict)
{
var _local6:int = index++;
array[_local6] = key;
var _local7:int = index++;
array[_local7] = _dict[key];
};
return (array);
}
public function toObject():Object
{
return (toArray());
}
public function fromObject(object:Object):void
{
clear();
var index:uint;
while (index < (object as Array).length) {
set((object as Array)[index], (object as Array)[(index + 1)]);
index += 2;
};
}
public function get size():uint
{
return (_size);
}
}
import flash.utils.ByteArray;
/**
* a piece of flv, fetch from cdn or p2p.
*/
class FlvPiece
{
private var _pieceId:Number;
protected var _flv:ByteArray;
/**
* the private object for the channel,
* for example, the cdn channel will set to CdnEdge object.
*/
private var _privateObject:Object;
/**
* when encoder error, this piece cannot be generated,
* and it should be skip. default to false.
*/
private var _skip:Boolean;
public function FlvPiece(pieceId:Number)
{
_pieceId = pieceId;
_flv = null;
_skip = false;
}
/**
* when piece is fetch ok.
*/
public function onPieceDone(flv:ByteArray):void
{
// save body.
_flv = flv;
}
/**
* when piece is fetch error.
*/
public function onPieceError():void
{
}
/**
* when piece is empty.
*/
public function onPieceEmpty():void
{
}
/**
* destroy the object, set reference to null.
*/
public function destroy():void
{
_privateObject = null;
_flv = null;
}
public function get privateObject():Object
{
return _privateObject;
}
public function set privateObject(v:Object):void
{
_privateObject = v;
}
public function get skip():Boolean
{
return _skip;
}
public function set skip(v:Boolean):void
{
_skip = v;
}
public function get pieceId():Number
{
return _pieceId;
}
public function get flv():ByteArray
{
return _flv;
}
public function get completed():Boolean
{
return _flv != null;
}
}
interface ILogger
{
function debug0(message:String, ... rest):void;
function debug(message:String, ... rest):void;
function info(message:String, ... rest):void;
function warn(message:String, ... rest):void;
function error(message:String, ... rest):void;
function fatal(message:String, ... rest):void;
}
import flash.globalization.DateTimeFormatter;
import flash.external.ExternalInterface;
class TraceLogger implements ILogger
{
private var _category:String;
public function get category():String
{
return _category;
}
public function TraceLogger(category:String)
{
_category = category;
}
public function debug0(message:String, ...rest):void
{
}
public function debug(message:String, ...rest):void
{
}
public function info(message:String, ...rest):void
{
logMessage(LEVEL_INFO, message, rest);
}
public function warn(message:String, ...rest):void
{
logMessage(LEVEL_WARN, message, rest);
}
public function error(message:String, ...rest):void
{
logMessage(LEVEL_ERROR, message, rest);
}
public function fatal(message:String, ...rest):void
{
logMessage(LEVEL_FATAL, message, rest);
}
protected function logMessage(level:String, message:String, params:Array):void
{
var msg:String = "";
// add datetime
var date:Date = new Date();
var dtf:DateTimeFormatter = new DateTimeFormatter("UTC");
dtf.setDateTimePattern("yyyy-MM-dd HH:mm:ss");
// TODO: FIXME: the SSS format not run, use date.milliseconds instead.
msg += '[' + dtf.format(date) + "." + date.milliseconds + ']';
msg += " [" + level + "] ";
// add category and params
msg += "[" + category + "] " + applyParams(message, params);
// trace the message
trace(msg);
if (!flash.external.ExternalInterface.available) {
return;
}
ExternalInterface.call("console.log", msg);
}
private function leadingZeros(x:Number):String
{
if (x < 10) {
return "00" + x.toString();
}
if (x < 100) {
return "0" + x.toString();
}
return x.toString();
}
private function applyParams(message:String, params:Array):String
{
var result:String = message;
var numParams:int = params.length;
for (var i:int = 0; i < numParams; i++) {
result = result.replace(new RegExp("\\{" + i + "\\}", "g"), params[i]);
}
return result;
}
private static const LEVEL_DEBUG:String = "DEBUG";
private static const LEVEL_WARN:String = "WARN";
private static const LEVEL_INFO:String = "INFO";
private static const LEVEL_ERROR:String = "ERROR";
private static const LEVEL_FATAL:String = "FATAL";
} }
import flash.utils.ByteArray; import flash.utils.ByteArray;
@ -304,7 +883,7 @@ class SrsTsHanlder implements ISrsTsHandler
private var queue:Array; private var queue:Array;
// hls data. // hls data.
private var _hls:Hls; private var _hls:HlsCodec;
private var _body:ByteArray; private var _body:ByteArray;
private var _on_size_changed:Function; private var _on_size_changed:Function;
private var _on_sequence_changed:Function; private var _on_sequence_changed:Function;
@ -316,7 +895,7 @@ class SrsTsHanlder implements ISrsTsHandler
ph264_sps:ByteArray, ph264_pps:ByteArray, ph264_sps:ByteArray, ph264_pps:ByteArray,
paac_specific_config:ByteArray, paac_specific_config:ByteArray,
pvideo_sh_tag:ByteArray, paudio_sh_tag:ByteArray, pvideo_sh_tag:ByteArray, paudio_sh_tag:ByteArray,
hls:Hls, body:ByteArray, oszc:Function, oshc:Function) hls:HlsCodec, body:ByteArray, oszc:Function, oshc:Function)
{ {
_hls = hls; _hls = hls;
_body = body; _body = body;
@ -2383,7 +2962,7 @@ interface ISrsTsHandler
*/ */
class SrsTsContext class SrsTsContext
{ {
private var _hls:Hls; private var _hls:HlsCodec;
// codec // codec
// key, a Number indicates the pid, // key, a Number indicates the pid,
@ -2393,7 +2972,7 @@ class SrsTsContext
// whether hls pure audio stream. // whether hls pure audio stream.
private var _pure_audio:Boolean; private var _pure_audio:Boolean;
public function SrsTsContext(hls:Hls) public function SrsTsContext(hls:HlsCodec)
{ {
_hls = hls; _hls = hls;
_pure_audio = false; _pure_audio = false;
@ -2620,7 +3199,7 @@ class SrsTsPacket
} }
// calc the user defined data size for payload. // calc the user defined data size for payload.
var nb_payload:int = Hls.SRS_TS_PACKET_SIZE - (stream.position - pos); var nb_payload:int = HlsCodec.SRS_TS_PACKET_SIZE - (stream.position - pos);
// optional: payload. // optional: payload.
if (adaption_field_control == SrsTsAdaptationFieldType.PayloadOnly if (adaption_field_control == SrsTsAdaptationFieldType.PayloadOnly
@ -4583,7 +5162,7 @@ class SrsTsPayloadPMT extends SrsTsPayloadPSI
*/ */
class M3u8 class M3u8
{ {
private var _hls:Hls; private var _hls:HlsCodec;
private var _log:ILogger = new TraceLogger("HLS"); private var _log:ILogger = new TraceLogger("HLS");
private var _tses:Array; private var _tses:Array;
@ -4593,7 +5172,7 @@ class M3u8
// when variant, all ts url is sub m3u8 url. // when variant, all ts url is sub m3u8 url.
private var _variant:Boolean; private var _variant:Boolean;
public function M3u8(hls:Hls) public function M3u8(hls:HlsCodec)
{ {
_hls = hls; _hls = hls;
_tses = new Array(); _tses = new Array();

View file

@ -1,12 +0,0 @@
package
{
public interface ILogger
{
function debug0(message:String, ... rest):void;
function debug(message:String, ... rest):void;
function info(message:String, ... rest):void;
function warn(message:String, ... rest):void;
function error(message:String, ... rest):void;
function fatal(message:String, ... rest):void;
}
}

View file

@ -1,33 +0,0 @@
package
{
import flash.net.NetStream;
/**
* the player interface.
*/
public interface IPlayer
{
/**
* initialize the player by flashvars for config.
* @param flashvars the config.
*/
function init(flashvars:Object):void;
/**
* get the NetStream to play the stream.
* @return the underlayer stream object.
*/
function stream():NetStream;
/**
* connect and play url.
* @param url the stream url to play.
*/
function play(url:String):void;
/**
* close the player.
*/
function close():void;
}
}

View file

@ -1,307 +0,0 @@
package
{
import flash.display.Sprite;
import flash.display.StageAlign;
import flash.display.StageDisplayState;
import flash.display.StageScaleMode;
import flash.events.Event;
import flash.events.FullScreenEvent;
import flash.events.MouseEvent;
import flash.events.NetStatusEvent;
import flash.events.ProgressEvent;
import flash.events.TimerEvent;
import flash.external.ExternalInterface;
import flash.media.SoundTransform;
import flash.media.Video;
import flash.net.NetConnection;
import flash.net.NetStream;
import flash.net.NetStreamAppendBytesAction;
import flash.net.URLLoader;
import flash.net.URLLoaderDataFormat;
import flash.net.URLRequest;
import flash.net.URLRequestHeader;
import flash.net.URLRequestMethod;
import flash.net.URLStream;
import flash.net.URLVariables;
import flash.system.Security;
import flash.ui.ContextMenu;
import flash.ui.ContextMenuItem;
import flash.utils.ByteArray;
import flash.utils.Timer;
import flash.utils.getTimer;
import flash.utils.setTimeout;
import flashx.textLayout.formats.Float;
/**
* the m3u8 player.
*/
public class M3u8Player implements IPlayer
{
private var js_id:String = null;
// play param url.
private var user_url:String = null;
private var media_stream:NetStream = null;
private var media_conn:NetConnection = null;
private var owner:srs_player = null;
private var hls:Hls = null; // parse m3u8 and ts
// the uuid similar to Safari, to identify this play session.
// @see https://github.com/winlinvip/srs-plus/blob/bms/trunk/src/app/srs_app_http_stream.cpp#L45
public var XPlaybackSessionId:String = createRandomIdentifier(32);
private function createRandomIdentifier(length:uint, radix:uint = 61):String {
var characters:Array = new Array('0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q',
'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h',
'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y',
'z');
var id:Array = new Array();
radix = (radix > 61) ? 61 : radix;
while (length--) {
id.push(characters[randomIntegerWithinRange(0, radix)]);
}
return id.join('');
}
private function randomIntegerWithinRange(min:int, max:int):int {
return Math.floor(Math.random() * (1 + max - min) + min);
}
// callback for hls.
public var flvHeader:ByteArray = null;
public function onSequenceHeader():void {
if (!media_stream) {
setTimeout(onSequenceHeader, 1000);
return;
}
var s:NetStream = media_stream;
s.appendBytesAction(NetStreamAppendBytesAction.RESET_BEGIN);
s.appendBytes(flvHeader);
log("FLV: sps/pps " + flvHeader.length + " bytes");
writeFlv(flvHeader);
}
public function onFlvBody(uri:String, flv:ByteArray):void {
if (!media_stream) {
return;
}
if (!flvHeader) {
return;
}
var s:NetStream = media_stream;
s.appendBytes(flv);
log("FLV: ts " + uri + " parsed to flv " + flv.length + " bytes");
writeFlv(flv);
}
private function writeFlv(data:ByteArray):void {
return;
var r:URLRequest = new URLRequest("http://192.168.1.117:8088/api/v1/flv");
r.method = URLRequestMethod.POST;
r.data = data;
var pf:URLLoader = new URLLoader();
pf.dataFormat = URLLoaderDataFormat.BINARY;
pf.load(r);
}
public function M3u8Player(o:srs_player) {
owner = o;
hls = new Hls(this);
}
public function init(flashvars:Object):void {
this.js_id = flashvars.id;
}
public function stream():NetStream {
return this.media_stream;
}
// owner.on_player_metadata(evt.info.data);
public function play(url:String):void {
var streamName:String;
this.user_url = url;
this.media_conn = new NetConnection();
this.media_conn.client = {};
this.media_conn.client.onBWDone = function():void {};
this.media_conn.addEventListener(NetStatusEvent.NET_STATUS, function(evt:NetStatusEvent):void {
log("NetConnection: code=" + evt.info.code + ", data is " + evt.info.data);
// TODO: FIXME: failed event.
if (evt.info.code != "NetConnection.Connect.Success") {
return;
}
media_stream = new NetStream(media_conn);
media_stream.addEventListener(NetStatusEvent.NET_STATUS, function(evt:NetStatusEvent):void {
log("NetStream: code=" + evt.info.code);
if (evt.info.code == "NetStream.Video.DimensionChange") {
owner.on_player_dimension_change();
} else if (evt.info.code == "NetStream.Buffer.Empty") {
owner.on_player_buffer_empty();
} else if (evt.info.code == "NetStream.Buffer.Full") {
owner.on_player_buffer_full();
}
// TODO: FIXME: failed event.
});
// setup stream before play.
owner.on_player_before_play();
media_stream.play(null);
refresh_m3u8();
owner.on_player_play();
});
this.media_conn.connect(null);
}
public function close():void {
if (this.media_stream) {
this.media_stream.close();
this.media_stream = null;
}
if (this.media_conn) {
this.media_conn.close();
this.media_conn = null;
}
}
private var parsed_ts_seq_no:Number = -1;
private function refresh_m3u8():void {
download(user_url, function(stream:ByteArray):void {
var m3u8:String = stream.toString();
hls.parse(user_url, m3u8);
// redirect by variant m3u8.
if (hls.variant) {
var smu:String = hls.getTsUrl(0);
log("variant hls=" + user_url + ", redirect2=" + smu);
user_url = smu;
setTimeout(refresh_m3u8, 0);
return;
}
// fetch from the last one.
if (parsed_ts_seq_no == -1) {
parsed_ts_seq_no = hls.seq_no + hls.tsCount - 1;
}
// not changed.
if (parsed_ts_seq_no >= hls.seq_no + hls.tsCount) {
refresh_ts();
return;
}
// parse each ts.
var nb_ts:Number = hls.seq_no + hls.tsCount - parsed_ts_seq_no;
log("m3u8 changed, got " + nb_ts + " new ts, count=" + hls.tsCount + ", seqno=" + hls.seq_no + ", parsed=" + parsed_ts_seq_no);
refresh_ts();
})
}
private function refresh_ts():void {
// all ts parsed.
if (parsed_ts_seq_no >= hls.seq_no + hls.tsCount) {
var to:Number = 1000;
if (hls.tsCount > 0) {
to = hls.duration * 1000 / hls.tsCount * Consts.M3u8RefreshRatio;
}
setTimeout(refresh_m3u8, to);
log("m3u8 not changed, retry after " + to.toFixed(2) + "ms");
return;
}
// parse current ts.
var uri:String = hls.getTsUrl(parsed_ts_seq_no - hls.seq_no);
// parse metadata from uri.
if (uri.indexOf("?") >= 0) {
var uv:URLVariables = new URLVariables(uri.substr(uri.indexOf("?") + 1));
var obj:Object = {};
for (var k:String in uv) {
var v:String = uv[k];
if (k == "shp_sip1") {
obj.srs_server_ip = v;
} else if (k == "shp_cid") {
obj.srs_id = v;
} else if (k == "shp_pid") {
obj.srs_pid = v;
}
//log("uv[" + k + "]=" + v);
}
owner.on_player_metadata(obj);
}
download(uri, function(stream:ByteArray):void{
log("got ts seqno=" + parsed_ts_seq_no + ", " + stream.length + " bytes");
var flv:FlvPiece = new FlvPiece(parsed_ts_seq_no);
var body:ByteArray = new ByteArray();
stream.position = 0;
hls.parseBodyAsync(flv, stream, body, function():void{
body.position = 0;
//log("ts parsed, seqno=" + parsed_ts_seq_no + ", flv=" + body.length + "B");
onFlvBody(uri, body);
parsed_ts_seq_no++;
setTimeout(refresh_ts, 0);
});
});
}
private function download(uri:String, completed:Function):void {
var url:URLStream = new URLStream();
var stream:ByteArray = new ByteArray();
url.addEventListener(ProgressEvent.PROGRESS, function(evt:ProgressEvent):void {
if (url.bytesAvailable <= 0) {
return;
}
//log(uri + " total=" + evt.bytesTotal + ", loaded=" + evt.bytesLoaded + ", available=" + url.bytesAvailable);
var bytes:ByteArray = new ByteArray();
url.readBytes(bytes, 0, url.bytesAvailable);
stream.writeBytes(bytes);
});
url.addEventListener(Event.COMPLETE, function(evt:Event):void {
log(uri + " completed, total=" + stream.length + "bytes");
if (url.bytesAvailable <= 0) {
completed(stream);
return;
}
//log(uri + " completed" + ", available=" + url.bytesAvailable);
var bytes:ByteArray = new ByteArray();
url.readBytes(bytes, 0, url.bytesAvailable);
stream.writeBytes(bytes);
completed(stream);
});
// we set to the query.
uri += ((uri.indexOf("?") == -1)? "?":"&") + "shp_xpsid=" + XPlaybackSessionId;
var r:URLRequest = new URLRequest(uri);
// seems flash not allow set this header.
r.requestHeaders.push(new URLRequestHeader("X-Playback-Session-Id", XPlaybackSessionId));
log("start download " + uri);
url.load(r);
}
private function log(msg:String):void {
Utility.log(js_id, msg);
}
}
}

View file

@ -28,8 +28,14 @@ package
* common player to play rtmp/flv stream, * common player to play rtmp/flv stream,
* use system NetStream. * use system NetStream.
*/ */
public class CommonPlayer implements IPlayer public class Player
{ {
// refresh every ts_fragment_seconds*M3u8RefreshRatio
public static var M3u8RefreshRatio:Number = 0.5;
// parse ts every this ms.
public static var TsParseAsyncInterval:Number = 80;
private var js_id:String = null; private var js_id:String = null;
// play param url. // play param url.
@ -40,7 +46,7 @@ package
private var owner:srs_player = null; private var owner:srs_player = null;
public function CommonPlayer(o:srs_player) { public function Player(o:srs_player) {
owner = o; owner = o;
} }
@ -88,7 +94,11 @@ package
return; return;
} }
if (url.indexOf(".m3u8") > 0) {
media_stream = new HlsNetStream(M3u8RefreshRatio, TsParseAsyncInterval, media_conn);
} else {
media_stream = new NetStream(media_conn); media_stream = new NetStream(media_conn);
}
media_stream.addEventListener(NetStatusEvent.NET_STATUS, function(evt:NetStatusEvent):void { media_stream.addEventListener(NetStatusEvent.NET_STATUS, function(evt:NetStatusEvent):void {
log("NetStream: code=" + evt.info.code); log("NetStream: code=" + evt.info.code);

View file

@ -1,93 +0,0 @@
package
{
import flash.globalization.DateTimeFormatter;
public class TraceLogger implements ILogger
{
private var _category:String;
public function get category():String
{
return _category;
}
public function TraceLogger(category:String)
{
_category = category;
}
public function debug0(message:String, ...rest):void
{
}
public function debug(message:String, ...rest):void
{
}
public function info(message:String, ...rest):void
{
logMessage(LEVEL_INFO, message, rest);
}
public function warn(message:String, ...rest):void
{
logMessage(LEVEL_WARN, message, rest);
}
public function error(message:String, ...rest):void
{
logMessage(LEVEL_ERROR, message, rest);
}
public function fatal(message:String, ...rest):void
{
logMessage(LEVEL_FATAL, message, rest);
}
protected function logMessage(level:String, message:String, params:Array):void
{
var msg:String = "";
// add datetime
var date:Date = new Date();
var dtf:DateTimeFormatter = new DateTimeFormatter("UTC");
dtf.setDateTimePattern("yyyy-MM-dd HH:mm:ss");
// TODO: FIXME: the SSS format not run, use date.milliseconds instead.
msg += '[' + dtf.format(date) + "." + date.milliseconds + ']';
msg += " [" + level + "] ";
// add category and params
msg += "[" + category + "] " + applyParams(message, params);
// trace the message
Utility.log("CORE", msg);
}
private function leadingZeros(x:Number):String
{
if (x < 10) {
return "00" + x.toString();
}
if (x < 100) {
return "0" + x.toString();
}
return x.toString();
}
private function applyParams(message:String, params:Array):String
{
var result:String = message;
var numParams:int = params.length;
for (var i:int = 0; i < numParams; i++) {
result = result.replace(new RegExp("\\{" + i + "\\}", "g"), params[i]);
}
return result;
}
private static const LEVEL_DEBUG:String = "DEBUG";
private static const LEVEL_WARN:String = "WARN";
private static const LEVEL_INFO:String = "INFO";
private static const LEVEL_ERROR:String = "ERROR";
private static const LEVEL_FATAL:String = "FATAL";
}
}

View file

@ -32,14 +32,16 @@ package
* @param msg the log message. * @param msg the log message.
*/ */
public static function log(js_id:String, msg:String):void { public static function log(js_id:String, msg:String):void {
if (js_id) {
msg = "[" + new Date() +"][srs-player][" + js_id + "] " + msg; msg = "[" + new Date() +"][srs-player][" + js_id + "] " + msg;
}
logData += msg + "\n"; logData += msg + "\n";
trace(msg); trace(msg);
if (!flash.external.ExternalInterface.available) { if (!flash.external.ExternalInterface.available) {
flash.utils.setTimeout(log, 300, msg); flash.utils.setTimeout(log, 300, null, msg);
return; return;
} }

View file

@ -58,7 +58,7 @@ package
private var control_fs_mask:Sprite = new Sprite(); private var control_fs_mask:Sprite = new Sprite();
// the common player to play stream. // the common player to play stream.
private var player:IPlayer = null; private var player:Player = null;
// the flashvars config. // the flashvars config.
private var config:Object = null; private var config:Object = null;
@ -438,13 +438,7 @@ package
} }
// create player. // create player.
if (url.indexOf(".m3u8") > 0 && Utility.stringStartswith(url, "http://")) { player = new Player(this);
player = new M3u8Player(this);
log("create M3U8 player.");
} else {
player = new CommonPlayer(this);
log("create Common player.");
}
// init player by config. // init player by config.
player.init(config); player.init(config);