mirror of https://github.com/ossrs/srs.git
refine srs player for hls, extract a HlsNetStream.
parent
2492562d25
commit
77167f3331
Binary file not shown.
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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";
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue