source: trunk/as3/com/jeroenwijering/models/RTMPModel.as @ 760

Revision 760, 13.6 KB checked in by jeroen, 3 years ago (diff)

added fix for http bitrate switch on LOAD, saved last bandwidth in a cookie, evened out FMS3.5 bandwidth callbacks and added rtmpt to the testing bitrate switches (can be throttles with Charles)

  • Property svn:executable set to *
Line 
1package com.jeroenwijering.models {
2
3
4import com.jeroenwijering.events.*;
5import com.jeroenwijering.models.AbstractModel;
6import com.jeroenwijering.player.Model;
7import com.jeroenwijering.utils.*;
8
9import flash.events.*;
10import flash.media.*;
11import flash.net.*;
12import flash.utils.*;
13
14
15/**
16* Wrapper for playback of video streamed over RTMP. Can playback MP4, FLV, MP3, AAC and live streams.
17* Server-specific features are:
18* - The SecureToken functionality of Wowza (with the 'token' flahvar).
19* - Load balancing with SMIL files (with the 'rtmp.loadbalance=true' flashvar).
20**/
21public class RTMPModel extends AbstractModel {
22
23
24        /** Save if the bandwidth checkin already occurs. **/
25        private var bwcheck:Boolean;
26        /** Interval for bw checking - with dynamic streaming. **/
27        private var bwinterval:Number;
28        /** NetConnection object for setup of the video stream. **/
29        private var connection:NetConnection;
30        /** Is dynamic streaming possible. **/
31        private var dynamics:Boolean;
32        /** The currently playing RTMP stream. **/
33        private var file:String;
34        /** ID for the position interval. **/
35        private var interval:Number;
36        /** Loader instance that loads the XML file. **/
37        private var loader:URLLoader;
38        /** NetStream instance that handles the stream IO. **/
39        private var stream:NetStream;
40        /** Interval ID for subscription pings. **/
41        private var subscribe:Number;
42        /** Offset in seconds of the last seek. **/
43        private var timeoffset:Number = 0;
44        /** Sound control object. **/
45        private var transformer:SoundTransform;
46        /** Save the location of the XML redirect. **/
47        private var smil:String;
48        /** Save that a stream is streaming. **/
49        private var streaming:Boolean;
50        /** Save that we're transitioning. **/
51        private var transitioning:Boolean;
52        /** Video object to be instantiated. **/
53        private var video:Video;
54
55        /** Constructor; sets up the connection and display. **/
56        public function RTMPModel(mod:Model):void {
57                super(mod);
58                connection = new NetConnection();
59                connection.addEventListener(NetStatusEvent.NET_STATUS,statusHandler);
60                connection.addEventListener(SecurityErrorEvent.SECURITY_ERROR,errorHandler);
61                connection.addEventListener(IOErrorEvent.IO_ERROR,errorHandler);
62                connection.addEventListener(AsyncErrorEvent.ASYNC_ERROR,errorHandler);
63                connection.objectEncoding = ObjectEncoding.AMF0;
64                connection.client = new NetClient(this);
65                loader = new URLLoader();
66                loader.addEventListener(Event.COMPLETE, loaderHandler);
67                loader.addEventListener(SecurityErrorEvent.SECURITY_ERROR,errorHandler);
68                loader.addEventListener(IOErrorEvent.IO_ERROR,errorHandler);
69                transformer = new SoundTransform();
70                video = new Video(320,240);
71                video.smoothing = model.config['smoothing'];
72                addChild(video);
73        };
74
75
76        /** Check if the player can use dynamic streaming (server versions and no load balancing). **/
77        private function checkDynamic(str:String):void {
78                var clt:Number = Number(model.config['client'].split(' ')[2].split(',')[0]);
79                var mjr:Number = Number(str.split(',')[0]);
80                var mnr:Number = Number(str.split(',')[1]);
81                if(!model.config['rtmp.loadbalance'] && !item['rtmp.loadbalance'] &&
82                        clt > 9 && (mjr > 3 || (mjr == 3 && mnr > 4))) {
83                        dynamics = true;
84                } else {
85                        dynamics = false;
86                }
87        };
88
89
90        /** Try subscribing to livestream **/
91        private function doSubscribe(id:String):void {
92                connection.call("FCSubscribe",null,id);
93        };
94
95
96        /** Catch security errors. **/
97        private function errorHandler(evt:ErrorEvent):void {
98                stop();
99                model.sendEvent(ModelEvent.ERROR,{message:evt.text});
100        };
101
102
103        /** Bandwidth checking for dynamic streaming. **/
104        private function getBandwidth():void {
105                try {
106                        var bdw:Number = Math.round(stream.info.maxBytesPerSecond*8/1024);
107                } catch(err:Error) {
108                        clearInterval(bwinterval);
109                        return;
110                }
111                if(bdw < 100 || bdw > 99999) {
112                        return;
113                } else {
114                        bdw = Math.round(model.config['bandwidth']/2+bdw/2);
115                }
116                model.config['bandwidth'] = bdw;
117                Configger.saveCookie('bandwidth',bdw);
118                if(item['levels'] && getLevel() != model.config['level']) {
119                        swap();
120                }
121        };
122
123
124        /** Extract the correct rtmp syntax from the file string. **/
125        private function getID(url:String):String {
126                var ext:String = url.substr(-4);
127                if(url.indexOf(':') > -1) {
128                        return url;
129                } else if(ext == '.mp3') {
130                        return 'mp3:'+url.substr(0,url.length-4);
131                } else if (ext=='.mp4' || ext=='.mov' || ext=='.m4v' || ext=='.aac' || ext=='.m4a' || ext=='.f4v') {
132                        return 'mp4:'+url;
133                } else if (ext == '.flv') {
134                        return url.substr(0,url.length-4);
135                } else {
136                        return url;
137                }
138        };
139
140
141        /** Return which level best fits the display width and connection bandwidth. **/
142        private function getLevel():Number {
143                var lvl:Number = item['levels'].length-1;
144                for (var i:Number=0; i<item['levels'].length; i++) {
145                        if(model.config['width'] >= item['levels'][i].width &&
146                                model.config['bandwidth'] >= item['levels'][i].bitrate) {
147                                lvl = i;
148                                break;
149                        }
150                }
151                return lvl;
152        };
153
154
155        /** Load content. **/
156        override public function load(itm:Object):void {
157                item = itm;
158                position = 0;
159                if(item['levels']) { loadLevelSync(); }
160                timeoffset = item['start'];
161                clearInterval(interval);
162                model.sendEvent(ModelEvent.STATE,{newstate:ModelStates.BUFFERING});
163                if(model.config['rtmp.loadbalance'] || item['rtmp.loadbalance']) {
164                        smil = item['file'];
165                        loader.load(new URLRequest(smil));
166                } else {
167                        connection.connect(item['streamer']);
168                }
169        };
170
171
172        /** Make sure the selected level is actually the item['file']. **/
173        private function loadLevelSync() {
174                for (var i:Number=0; i<item['levels'].length; i++) {
175                        if(item['file'] == item['levels'][i].url ) {
176                                model.config['level'] = i;
177                                break;
178                        }
179                }
180        };
181
182
183
184        /** Get the streamer / file from the loadbalancing XML. **/
185        private function loaderHandler(evt:Event):void {
186                var xml:XML = XML(evt.currentTarget.data);
187                item['streamer'] = xml.children()[0].children()[0].@base.toString();
188                item['file'] = xml.children()[1].children()[0].@src.toString();
189                connection.connect(item['streamer']);
190        };
191
192
193        /** Get metadata information from netstream class. **/
194        public function onClientData(dat:Object):void {
195                if(dat.type == 'fcsubscribe') {
196                        if(dat.code == "NetStream.Play.StreamNotFound" ) {
197                                model.sendEvent(ModelEvent.ERROR,{message:"Subscription failed: "+item['file']});
198                        } else if(dat.code == "NetStream.Play.Start") {
199                                setStream();
200                        }
201                        clearInterval(subscribe);
202                }
203                if(dat.width) {
204                        video.width = dat.width;
205                        video.height = dat.height;
206                        super.resize();
207                }
208                if(dat.duration && !item['duration']) {
209                        item['duration'] = dat.duration;
210                }
211                if(dat.type == 'complete') {
212                        clearInterval(interval);
213                        model.sendEvent(ModelEvent.STATE,{newstate:ModelStates.COMPLETED});
214                }
215                if(dat.type == 'close') {
216                        stop();
217                }
218                if(dat.type == 'bandwidth') {
219                        model.config['bandwidth'] = dat.bandwidth;
220                        Configger.saveCookie('bandwidth',dat.bandwidth);
221                        setStream();
222                }
223                if(dat.code == 'NetStream.Play.TransitionComplete') {
224                        transitioning = false;
225                }
226                model.sendEvent(ModelEvent.META,dat);
227        };
228
229
230        /** Pause playback. **/
231        override public function pause():void {
232                stream.pause();
233                clearInterval(interval);
234                model.sendEvent(ModelEvent.STATE,{newstate:ModelStates.PAUSED});
235                if(stream && item['duration'] == 0 && !dynamics) { stop(); }
236        };
237
238
239        /** Resume playing. **/
240        override public function play():void {
241                stream.resume();
242                interval = setInterval(positionInterval,100);
243                model.sendEvent(ModelEvent.STATE,{newstate:ModelStates.PLAYING});
244        };
245
246
247        /** Interval for the position progress. **/
248        private function positionInterval():void {
249                var pos:Number = Math.round((stream.time)*10)/10;
250                var bfr:Number = stream.bufferLength/stream.bufferTime;
251                if(bfr < 0.25 && pos < item['duration']-5 && model.config['state'] != ModelStates.BUFFERING) {
252                        model.sendEvent(ModelEvent.STATE,{newstate:ModelStates.BUFFERING});
253                        stream.bufferTime = model.config['bufferlength'];
254                } else if (bfr > 1 && model.config['state'] != ModelStates.PLAYING) {
255                        model.sendEvent(ModelEvent.STATE,{newstate:ModelStates.PLAYING});
256                        stream.bufferTime = model.config['bufferlength']*4;
257                }
258                if(model.config['state'] != ModelStates.PLAYING) {
259                        return;
260                }
261                if(pos < item['duration']) {
262                        model.sendEvent(ModelEvent.TIME,{position:pos,duration:item['duration']});
263                        position = pos;
264                } else if (position > 0 && item['duration'] > 0) {
265                        stream.pause();
266                        clearInterval(interval);
267                        if(stream && item['duration'] == 0) { stop(); }
268                        model.sendEvent(ModelEvent.STATE,{newstate:ModelStates.COMPLETED});
269                }
270        };
271
272
273        /** Check if the level must be switched on resize. **/
274        override public function resize():void {
275                super.resize();
276                if(item['levels'] && getLevel() != model.config['level'] &&
277                        model.config['state'] == ModelStates.PLAYING) {
278                        if(dynamics) {
279                                swap();
280                        } else {
281                                seek(position);
282                        }
283                }
284        };
285
286
287        /** Seek to a new position. **/
288        override public function seek(pos:Number):void {
289                position = 0;
290                timeoffset = pos;
291                transitioning = false;
292                clearInterval(interval);
293                clearInterval(bwinterval);
294                if(item['levels'] && getLevel() != model.config['level']) {
295                        model.config['level'] = getLevel();
296                        item['file'] = item['levels'][model.config['level']].url;
297                        if(model.config['rtmp.loadbalance'] || item['rtmp.loadbalance']) {
298                                item['start'] = pos;
299                                load(item);
300                                return;
301                        }
302                }
303                if(model.config['state'] == ModelStates.PAUSED) {
304                        stream.resume();
305                }
306                if(model.config['rtmp.subscribe'] || item['rtmp.subscribe']) {
307                        stream.play(getID(item['file']));
308                } else if(model.config['rtmp.dvr'] || item['rtmp.dvr']) {
309                        stream.play(getID(item['file']),0,-1);
310                } else {
311                        if(file != item['file']) {
312                                file = item['file'];
313                                stream.play(getID(item['file']));
314                        }
315                        if(timeoffset) { stream.seek(timeoffset); }
316                        if(dynamics) {
317                                bwinterval = setInterval(getBandwidth,2000);
318                        }
319                }
320                streaming = true;
321                interval = setInterval(positionInterval,100);
322        };
323
324
325        /** Start the netstream object. **/
326        private function setStream():void {
327                stream = new NetStream(connection);
328                stream.checkPolicyFile = true;
329                stream.addEventListener(NetStatusEvent.NET_STATUS,statusHandler);
330                stream.addEventListener(IOErrorEvent.IO_ERROR,errorHandler);
331                stream.addEventListener(AsyncErrorEvent.ASYNC_ERROR,errorHandler);
332                stream.bufferTime = model.config['bufferlength'];
333                stream.client = new NetClient(this);
334                video.attachNetStream(stream);
335                model.config['mute'] == true ? volume(0): volume(model.config['volume']);
336                seek(timeoffset);
337                resize();
338        };
339
340
341        /** Receive NetStream status updates. **/
342        private function statusHandler(evt:NetStatusEvent):void {
343                switch(evt.info.code) {
344                        case 'NetConnection.Connect.Success':
345                                if(evt.info.secureToken != undefined) {
346                                        connection.call("secureTokenResponse",null,
347                                                TEA.decrypt(evt.info.secureToken,model.config['token']));
348                                }
349                                if(evt.info.data) { checkDynamic(evt.info.data.version); }
350                                if(model.config['rtmp.subscribe'] || item['rtmp.subscribe']) {
351                                        subscribe = setInterval(doSubscribe,1000,getID(item['file']));
352                                        return;
353                                } else {
354                                        if(item['levels']) {
355                                                if(dynamics || bwcheck) {
356                                                        setStream();
357                                                } else {
358                                                        bwcheck = true;
359                                                        connection.call('checkBandwidth',null);
360                                                }
361                                        } else {
362                                                setStream();
363                                        }
364                                        if(item['file'].substr(-4) == '.mp3' || item['file'].substr(0,4) == 'mp3:') {
365                                                connection.call("getStreamLength",new Responder(streamlengthHandler),getID(item['file']));
366                                        }
367                                }
368                                break;
369                        case  'NetStream.Seek.Notify':
370                                clearInterval(interval);
371                                interval = setInterval(positionInterval,100);
372                                break;
373                        case 'NetConnection.Connect.Rejected':
374                                try {
375                                        if(evt.info.ex.code == 302) {
376                                                item['streamer'] = evt.info.ex.redirect;
377                                                setTimeout(load,100,item);
378                                                return;
379                                        }
380                                } catch (err:Error) {
381                                        stop();
382                                        var msg:String = evt.info.code;
383                                        if(evt.info['description']) { msg = evt.info['description']; }
384                                        stop();
385                                        model.sendEvent(ModelEvent.ERROR,{message:msg});
386                                }
387                                break;
388                        case 'NetStream.Failed':
389                        case 'NetStream.Play.StreamNotFound':
390                                if(!streaming) {
391                                        onClientData({type:'complete'});
392                                } else {
393                                        stop();
394                                        model.sendEvent(ModelEvent.ERROR,{message:"Stream not found: "+item['file']});
395                                }
396                                break;
397                        case 'NetConnection.Connect.Failed':
398                                stop();
399                                model.sendEvent(ModelEvent.ERROR,{message:"Server not found: "+item['streamer']});
400                                break;
401                        case 'NetStream.Play.Stop':
402                                if(model.config['rtmp.dvr']) { stop(); }
403                                break;
404                        case 'NetStream.Play.UnpublishNotify':
405                                stop();
406                                break;
407                }
408                model.sendEvent('META',evt.info);
409        };
410
411
412
413        /** Destroy the stream. **/
414        override public function stop():void {
415                if(stream && stream.time) { stream.close(); }
416                streaming = false;
417                file = undefined;
418                connection.close();
419                clearInterval(interval);
420                clearInterval(bwinterval);
421                position = 0;
422                timeoffset = item['start'];
423                model.sendEvent(ModelEvent.STATE,{newstate:ModelStates.IDLE});
424                if(smil) {
425                        item['file'] = smil;
426                }
427        };
428
429
430        /** Get the streamlength returned from the connection. **/
431        private function streamlengthHandler(len:Number):void {
432                Logger.log({duration:len});
433                if(len && !item['duration']) { item['duration'] = len; }
434        };
435
436
437        /** Dynamically switch streams **/
438        private function swap():void {
439                if(transitioning == true) {
440                        Logger.log('transition to level '+getLevel()+' cancelled');
441                } else {
442                        transitioning = true;
443                        model.config['level'] = getLevel();
444                        Logger.log('transition to level '+getLevel()+' initiated');
445                        item['file'] = file = item['levels'][model.config['level']].url;
446                        var nso:NetStreamPlayOptions = new NetStreamPlayOptions();
447                        nso.streamName = getID(item['file']);
448                        nso.transition = NetStreamPlayTransitions.SWITCH;
449                        stream.play2(nso);
450                }
451        };
452
453
454        /** Set the volume level. **/
455        override public function volume(vol:Number):void {
456                transformer.volume = vol/100;
457                if(stream) {
458                        stream.soundTransform = transformer;
459                }
460        };
461
462
463};
464
465
466}
Note: See TracBrowser for help on using the repository browser.