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

Revision 758, 13.2 KB checked in by pablo, 3 years ago (diff)

Branching off 4.7 dvr changes

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