source: trunk/fl5/src/com/longtailvideo/jwplayer/media/RTMPMediaProvider.as @ 824

Revision 824, 22.4 KB checked in by pablo, 3 years ago (diff)

Fixed issue with delayed MediaProvider loading

Line 
1/**
2 * Wrapper for playback of _video streamed over RTMP.
3 *
4 * All playback functionalities are cross-server (FMS, Wowza, Red5)
5 **/
6package com.longtailvideo.jwplayer.media {
7    import com.longtailvideo.jwplayer.events.MediaEvent;
8    import com.longtailvideo.jwplayer.events.PlayerEvent;
9    import com.longtailvideo.jwplayer.model.PlayerConfig;
10    import com.longtailvideo.jwplayer.model.PlaylistItem;
11    import com.longtailvideo.jwplayer.model.PlaylistItemLevel;
12    import com.longtailvideo.jwplayer.player.PlayerState;
13    import com.longtailvideo.jwplayer.utils.AssetLoader;
14    import com.longtailvideo.jwplayer.utils.Configger;
15    import com.longtailvideo.jwplayer.utils.Logger;
16    import com.longtailvideo.jwplayer.utils.NetClient;
17    import com.longtailvideo.jwplayer.utils.TEA;
18   
19    import flash.events.*;
20    import flash.media.*;
21    import flash.net.*;
22    import flash.utils.*;
23
24    /**
25     * Wrapper for playback of video streamed over RTMP. Can playback MP4, FLV, MP3, AAC and live streams.
26     * Server-specific features are:
27     * - The SecureToken functionality of Wowza (with the 'token' flahvar).
28     * - Load balancing with SMIL files (with the 'rtmp.loadbalance=true' flashvar).
29     **/
30    public class RTMPMediaProvider extends MediaProvider {
31                /** Save if the bandwidth checkin already occurs. **/
32                private var _bandwidthChecked:Boolean;
33        /** Interval for bw checking - with dynamic streaming. **/
34        private var _bandwidthInterval:Number;
35                /** Whether to connect to a stream when bandwidth is detected. **/
36                private var _bandwidthSwitch:Boolean;
37        /** NetConnection object for setup of the video stream. **/
38        private var _connection:NetConnection;
39        /** Is dynamic streaming possible. **/
40        private var _dynamic:Boolean;
41                /** The currently playing RTMP stream. **/
42                private var _currentFile:String;
43        /** ID for the position interval. **/
44        private var _positionInterval:Number;
45        /** Loaders for loading SMIL files. **/
46        private var _xmlLoaders:Dictionary;
47        /** NetStream instance that handles the stream IO. **/
48        private var _stream:NetStream;
49        /** Interval ID for subscription pings. **/
50        private var _subscribeInterval:Number;
51        /** Offset in seconds of the last seek. **/
52        private var _timeoffset:Number = -1;
53        /** Sound control object. **/
54        private var _transformer:SoundTransform;
55        /** Save that a stream is streaming. **/
56        private var _isStreaming:Boolean;
57        /** Level to which we're transitioning. **/
58        private var _transitionLevel:Number = -1;
59        /** Video object to be instantiated. **/
60        private var _video:Video;
61                /** Whether or not the buffer is full **/
62                private var _bufferFull:Boolean = false;
63                /** Duration of the DVR stream (grows with a timer). **/
64                private var _dvrDuration:Number = 0;
65                /** Total duration of the DVR stream (set by configuration). **/
66                private var _dvrTotalDuration:Number = 0;
67                /** If the item's duration should be set back to 0 on load. **/
68                private var _dvrResetDuration:Boolean = false;
69                /** How long to wait between updates to DVR duration **/
70                private var _dvrCheckDelay:Number = 1000;
71                /** Interval ID for growing the DVR duration. **/
72                private var _dvrInterval:Number;
73
74                public function RTMPMediaProvider() {
75                        super('rtmp');
76                }
77               
78               
79                /** Constructor; sets up the connection and display. **/
80                public override function initializeMediaProvider(cfg:PlayerConfig):void {
81                        super.initializeMediaProvider(cfg);
82            _connection = new NetConnection();
83            _connection.addEventListener(NetStatusEvent.NET_STATUS, statusHandler);
84            _connection.addEventListener(SecurityErrorEvent.SECURITY_ERROR, errorHandler);
85            _connection.addEventListener(IOErrorEvent.IO_ERROR, errorHandler);
86            _connection.addEventListener(AsyncErrorEvent.ASYNC_ERROR, errorHandler);
87            _connection.objectEncoding = ObjectEncoding.AMF0;
88            _connection.client = new NetClient(this);
89                        _xmlLoaders = new Dictionary();
90            _transformer = new SoundTransform();
91            _video = new Video(320, 240);
92            _video.smoothing = config.smoothing;
93        }
94
95        /** Check if the player can use dynamic streaming (server versions and no load balancing). **/
96        private function checkDynamic(str:String):void {
97            var clt:Number = Number(PlayerEvent.client.split(' ')[1].split(',')[0]);
98            var mjr:Number = Number(str.split(',')[0]);
99            var mnr:Number = Number(str.split(',')[1]);
100            if (!(getConfigProperty('loadbalance') && clt > 9 && (mjr > 3 || (mjr == 3 && mnr > 4)))) {
101                _dynamic = true;
102            } else {
103                _dynamic = false;
104            }
105        }
106
107        /** Try subscribing to livestream **/
108        private function doSubscribe(id:String):void {
109            _connection.call("FCSubscribe", null, id);
110        }
111
112                /** If there's a DVR stream, calcluate the position by incrementing it via a setInterval(). **/
113                private function dvrPosition():void {
114                        _dvrDuration += Math.ceil(_dvrCheckDelay / 1000);
115                        if(_dvrTotalDuration > 0) {
116                                var bufferPct:Number = Math.min(100, Math.ceil(100 * _dvrDuration / _dvrTotalDuration));
117                                sendBufferEvent(bufferPct);                     
118                        } else {
119                                if (item.duration == 0) { _dvrResetDuration = true; }
120                                item.duration = _dvrDuration;
121                        }
122                }
123
124        /** Catch security errors. **/
125        private function errorHandler(evt:ErrorEvent):void {
126            stop();
127                        sendMediaEvent(MediaEvent.JWPLAYER_MEDIA_ERROR, {message: evt.text});
128                }
129
130        /** Bandwidth checking for dynamic streaming. **/
131        private function getBandwidth():void {
132            try {
133                var bdw:Number = Math.round(_stream.info.maxBytesPerSecond * 8 / 1024);
134            } catch (err:Error) {
135                clearInterval(_bandwidthInterval);
136                return;
137            }
138            if (bdw < 100 || bdw > 99999) {
139                return;
140            } else {
141                bdw = Math.round(config.bandwidth / 2 + bdw / 2);
142            }
143            config.bandwidth = bdw;
144            Configger.saveCookie('bandwidth', bdw);
145            if (item.levels.length > 0 && item.getLevel(config.bandwidth, config.width) != item.currentLevel) {
146                swap(item.currentLevel);
147            }
148        }
149
150        /** Extract the correct rtmp syntax from the file string. **/
151        private function getID(url:String):String {
152            var ext:String = url.substr(-4);
153            if (url.indexOf(':') > -1) {
154                return url;
155            } else if (ext == '.mp3') {
156                return 'mp3:' + url.substr(0, url.length - 4);
157            } else if (ext == '.mp4' || ext == '.mov' || ext == '.m4v' || ext == '.aac' || ext == '.m4a' || ext == '.f4v') {
158                return 'mp4:' + url;
159            } else if (ext == '.flv') {
160                return url.substr(0, url.length - 4);
161            } else {
162                return url;
163            }
164        }
165
166        /** Load content. **/
167        override public function load(itm:PlaylistItem):void {
168            _item = itm;
169            _position = 0;
170                        _bufferFull = false;
171                        _bandwidthSwitch = false;                       
172            _timeoffset = item.start;
173                        if (item.levels.length > 0) { item.setLevel(item.getLevel(config.bandwidth, config.width)); }
174                       
175                        if (_dvrResetDuration) { item.duration = 0; }
176                        _dvrTotalDuration = item.duration;
177                        _dvrDuration = 0;
178                        clearInterval(_dvrInterval);
179                        _dvrInterval = 0;
180                       
181                        clearInterval(_positionInterval);
182                        setState(PlayerState.BUFFERING);
183                        sendBufferEvent(0);
184            if (getConfigProperty('loadbalance')) {
185                                loadSmil();
186                        } else {
187                                finishLoad();
188                        }
189        }
190
191                /** Load a SMIL file for load-balancing **/
192                private function loadSmil():void {
193                        if (!item.hasOwnProperty('smil')) {
194                                item.smil = [];                         
195                                if (item.levels.length > 0) {
196                                        for (var i:Number = 0; i < item.levels.length; i++) {
197                                                item.smil[i] = (item.levels[i] as PlaylistItemLevel).file;
198                                        }
199                                } else {
200                                        item.smil[0] = item.file;
201                                }
202                        }
203                       
204                        var smilFile:String = item.levels.length > 0 ? item.smil[item.currentLevel] : item.smil[0];
205                       
206                        var loader:AssetLoader = new AssetLoader();
207                        loader.addEventListener(Event.COMPLETE, loaderHandler);
208                        loader.addEventListener(ErrorEvent.ERROR, errorHandler);
209                        _xmlLoaders[loader] = smilFile;
210                        loader.load(smilFile, XML);
211                }
212               
213                /** Finalizes the loading process **/
214                private function finishLoad():void {
215                        if (item.file.substr(-4) == '.mp3') {
216                                media = null;
217                        } else if (!media) {
218                                media = _video;
219                        }
220                        sendMediaEvent(MediaEvent.JWPLAYER_MEDIA_LOADED);
221                        _connection.connect(item.streamer);
222                }
223               
224        /** Get the streamer / file from the loadbalancing XML. **/
225        private function loaderHandler(evt:Event):void {
226            var xml:XML = XML((evt.target as AssetLoader).loadedObject);
227                        var fileLocation:String = xml.body.video.@src.toString();
228                        var smilLocation:String = _xmlLoaders[evt.target];
229                        delete _xmlLoaders[evt.target];
230                        if (item.levels.length > 0) {
231                                var level:PlaylistItemLevel = item.levels[(item.smil as Array).indexOf(smilLocation)] as PlaylistItemLevel;
232                                level.streamer = xml.head.meta.@base.toString();
233                                level.file = fileLocation;
234                        } else {
235                                item.streamer = xml.head.meta.@base.toString();
236                        item.file = fileLocation;
237                        }
238                        finishLoad();
239        }
240
241        /** Get metadata information from netstream class. **/
242        public function onClientData(dat:Object):void {
243            if (dat.type == 'fcsubscribe') {
244                if (dat.code == "NetStream.Play.StreamNotFound") {
245                                        sendMediaEvent(MediaEvent.JWPLAYER_MEDIA_ERROR,{message: "Subscription failed: " + item.file});
246                } else if (dat.code == "NetStream.Play.Start") {
247                    setStream();
248                }
249                clearInterval(_subscribeInterval);
250            }
251            if (dat.width) {
252                                _video.width = dat.width;
253                                _video.height = dat.height;
254                                resize(_width, _height);
255            }
256            if (dat.duration) {
257                                if (isDVR) {
258                                        // Save the DVR duration differently, adding a small buffer.
259                                        _dvrDuration = dat.duration + 3;
260                                } else if (duration <= 0) {
261                        item.duration = dat.duration;
262                                }
263            }
264            if (dat.type == 'complete') {
265                clearInterval(_positionInterval);
266                                complete();
267            }
268            if (dat.type == 'close') {
269                stop();
270            }
271            if (dat.type == 'bandwidth') {
272                config.bandwidth = dat.bandwidth;
273                Configger.saveCookie('bandwidth', dat.bandwidth);
274                                if (_bandwidthSwitch) {
275                                        _bandwidthSwitch = false;
276                        setStream();
277                                }
278            }
279            if (dat.code == 'NetStream.Play.TransitionComplete') {
280                                if (_transitionLevel >= 0) {
281                                        Logger.log("Transition to level " + item.currentLevel + " complete");
282                        _transitionLevel = -1;
283                                }
284            }
285                        sendMediaEvent(MediaEvent.JWPLAYER_MEDIA_META, {metadata: dat});
286        }
287
288        /** Pause playback. **/
289        override public function pause():void {
290                        if (isLivestream) {
291                                stop();
292                                return;
293                        }
294                       
295                        clearInterval(_positionInterval);
296                        super.pause();
297            if (_stream) {
298                                Logger.log("NetStream.pause()");
299                                _stream.pause();
300                        }
301        }
302
303        /** Resume playing. **/
304        override public function play():void {
305                        clearInterval(_positionInterval);
306            if (state == PlayerState.PAUSED) {
307                                Logger.log("NetStream.resume()");
308                                _stream.resume();                               
309                        }
310                        super.play();
311                        _positionInterval = setInterval(positionInterval, 100);
312        }
313
314        /** Interval for the position progress. **/
315        private function positionInterval():void {
316            var pos:Number = Math.round((_stream.time) * 10) / 10;
317                        var bfr:Number = _stream.bufferLength / _stream.bufferTime;
318
319                        if (bfr < 0.25 && pos < duration - 5 && state != PlayerState.BUFFERING) {
320                                _bufferFull = false;
321                                setState(PlayerState.BUFFERING);
322            } else if (bfr > 1 && state != PlayerState.PLAYING) {
323                                if (state == PlayerState.BUFFERING && !isLivestream) {
324                                        _bufferFull = true;
325                                        sendMediaEvent(MediaEvent.JWPLAYER_MEDIA_BUFFER_FULL, {bufferPercent: bfr});
326                                }
327            }
328            if (state != PlayerState.PLAYING) {
329                return;
330            }
331            if (pos < duration) {
332                                _position = pos;
333                                sendMediaEvent(MediaEvent.JWPLAYER_MEDIA_TIME, {position: position, duration: duration});
334            } else if (position > 0 && duration > 0 && (!isDVR || _dvrTotalDuration > 0)) {
335                                Logger.log("NetStream.pause()");
336                _stream.pause();
337                clearInterval(_positionInterval);
338                                complete();
339            }
340        }
341
342        /** Check if the level must be switched on resize. **/
343        override public function resize(width:Number, height:Number):void {
344            super.resize(width, height);
345                        if (state == PlayerState.PLAYING) {
346                if (item.levels.length > 0 && item.currentLevel != item.getLevel(config.bandwidth, config.width)) {
347                        if (_dynamic) {
348                            swap(item.getLevel(config.bandwidth, config.width));
349                        } else {
350                            seek(position);
351                        }
352                                }
353            }
354        }
355
356        /** Seek to a new position. **/
357        override public function seek(pos:Number):void {
358                        if (isDVR && pos > _dvrDuration) { pos = _dvrDuration; }
359            _transitionLevel = -1;
360                        _timeoffset = pos;
361            clearInterval(_positionInterval);
362            clearInterval(_bandwidthInterval);
363                        if (item.levels.length > 0 && item.getLevel(config.bandwidth, config.width) != item.currentLevel) {
364                item.setLevel(item.getLevel(config.bandwidth, config.width));
365                if (getConfigProperty('loadbalance')) {
366                    item.start = pos;
367                    load(item);
368                    return;
369                }
370            }
371                        if (state == PlayerState.PAUSED) {
372                                play();
373                        }
374            if (getConfigProperty('subscribe')) {
375                                Logger.log("NetStream.play(" + getID(item.file) + ")");
376                _stream.play(getID(item.file));
377                        } else if(isDVR) {
378                                if(state != PlayerState.PLAYING) {
379                                        try {
380                                                _stream.play(getID(item.file),0,-1);
381                                        } catch(e:Error) {
382                                                error("Could not play DVR stream: " + e.message);
383                                        }
384                                }
385                                if(_timeoffset > 0) {
386                                        _stream.seek(_timeoffset);
387                                }
388                                if (!_dvrInterval) { _dvrInterval = setInterval(dvrPosition,1000); }
389            } else {
390                if (_currentFile != item.file) {
391                    _currentFile = item.file;
392                                        Logger.log("NetStream.play(" + getID(item.file) + ")");
393                                        try {
394                        _stream.play(getID(item.file));
395                                        } catch(e:Error) {
396                                                Logger.log("Error: " + e.message);
397                                        }
398                }
399                if (_timeoffset > 0 || state == PlayerState.IDLE) {
400                    if (_stream) {
401                                                Logger.log("NetStream.seek(" + _timeoffset + ")");
402                                                _stream.seek(_timeoffset);
403                                        }
404                }
405                if (_dynamic) {
406                    _bandwidthInterval = setInterval(getBandwidth, 2000);
407                }
408            }
409            _isStreaming = true;
410            _positionInterval = setInterval(positionInterval, 100);
411        }
412
413        /** Start the netstream object. **/
414        private function setStream():void {
415                        _stream = new NetStream(_connection);
416                        _stream.checkPolicyFile = true;
417                        _stream.addEventListener(NetStatusEvent.NET_STATUS, statusHandler);
418                        _stream.addEventListener(IOErrorEvent.IO_ERROR, errorHandler);
419                        _stream.addEventListener(AsyncErrorEvent.ASYNC_ERROR, errorHandler);
420                        _stream.bufferTime = config.bufferlength;
421                        _stream.client = new NetClient(this);
422                        _video.attachNetStream(_stream);
423                        config.mute == true ? setVolume(0) : setVolume(config.volume);
424                        seek(_timeoffset);
425        }
426
427        /** Receive NetStream status updates. **/
428        private function statusHandler(evt:NetStatusEvent):void {
429            switch (evt.info.code) {
430                case 'NetConnection.Connect.Success':
431                    if (evt.info.secureToken != undefined) {
432                        _connection.call("secureTokenResponse", null, TEA.decrypt(evt.info.secureToken,
433                                                                                  config.token));
434                    }
435                    if (evt.info.data) {
436                        checkDynamic(evt.info.data.version);
437                    }
438                    if (getConfigProperty('subscribe')) {
439                        _subscribeInterval = setInterval(doSubscribe, 1000, getID(item.file));
440                        return;
441                    } else {
442                        if (item.levels.length > 0) {
443                            if (_dynamic || _bandwidthChecked) {
444                                setStream();
445                            } else {
446                                                                _bandwidthChecked = true;
447                                                                _bandwidthSwitch = true;
448                                _connection.call('checkBandwidth', null);
449                            }
450                        } else {
451                            setStream();
452                        }
453                        if (item.file.substr(-4) == '.mp3' || item.file.substr(0,4) == 'mp3:') {
454                            _connection.call("getStreamLength", new Responder(streamlengthHandler), getID(item.file));
455                        }
456                    }
457                    break;
458                case 'NetStream.Seek.Notify':
459                    clearInterval(_positionInterval);
460                                        _positionInterval = setInterval(positionInterval, 100);
461                    break;
462                case 'NetConnection.Connect.Rejected':
463                    try {
464                        if (evt.info.ex.code == 302) {
465                            item.streamer = evt.info.ex.redirect;
466                            setTimeout(load, 100, item);
467                            return;
468                        }
469                    } catch (err:Error) {
470                        stop();
471                        var msg:String = evt.info.code;
472                        if (evt.info['description']) {
473                            msg = evt.info['description'];
474                        }
475                        stop();
476                                                sendMediaEvent(MediaEvent.JWPLAYER_MEDIA_ERROR, {message: msg});
477                    }
478                    break;
479                                case 'NetStream.Failed':
480                case 'NetStream.Play.StreamNotFound':
481                    if (!_isStreaming) {
482                        onClientData({type: 'complete'});
483                    } else {
484                        stop();
485                                                sendMediaEvent(MediaEvent.JWPLAYER_MEDIA_ERROR, {message: "Stream not found: " + item.file});
486                    }
487                    break;
488                                case 'NetStream.Seek.Failed':
489                                        if (!_isStreaming) {
490                                                onClientData({type: 'complete'});
491                                        } else {
492                                                stop();
493                                                sendMediaEvent(MediaEvent.JWPLAYER_MEDIA_ERROR, {message: "Could not seek: " + item.file});
494                                        }
495                                        break;
496                case 'NetConnection.Connect.Failed':
497                    stop();
498                                        sendMediaEvent(MediaEvent.JWPLAYER_MEDIA_ERROR, {message: "Server not found: " + item.streamer});
499                    break;
500                case 'NetStream.Play.UnpublishNotify':
501                    stop();
502                    break;
503                                case 'NetStream.Buffer.Full':
504                                        if (!_bufferFull) {
505                                                _bufferFull = true;
506                                                sendMediaEvent(MediaEvent.JWPLAYER_MEDIA_BUFFER_FULL);
507                                        }
508                                        break;
509                                case 'NetStream.Play.Transition':
510                                        onClientData(evt.info);
511                                        break;
512                                case 'NetStream.Play.Stop':
513                                        if(isDVR) { stop(); }
514                                        break;
515                                       
516            }
517                        sendMediaEvent(MediaEvent.JWPLAYER_MEDIA_META, {metadata: evt.info});
518        }
519
520        /** Destroy the stream. **/
521        override public function stop():void {
522            if (_stream && _stream.time) {
523                                Logger.log("NetStream.close()");
524                                _stream.close();
525            }
526            _isStreaming = false;
527            _currentFile = undefined;
528            _connection.close();
529            clearInterval(_positionInterval);
530            clearInterval(_bandwidthInterval);
531            _position = 0;
532            _timeoffset = item.start;
533                        super.stop();
534                        if (item.hasOwnProperty('smil')) {
535                                /** Replace file values with original redirects **/
536                                if (item.levels.length > 0) {
537                                        for each (var level:PlaylistItemLevel in item.levels) {
538                                                for (var i:Number = 0; i < (item.smil as Array).length; i++) {
539                                                        level.file = item.smil[i];
540                                                }
541                                        }
542                                } else {
543                                        item.file = item.smil[0];
544                                }
545                        }
546                }
547
548        /** Get the streamlength returned from the connection. **/
549        private function streamlengthHandler(len:Number):void {
550                        Logger.log("duration: " + len);
551                       
552                        if (isDVR && _dvrTotalDuration > 0) {
553                                _dvrDuration = len;
554                        } else if (!isDVR && len && duration <= 0) {
555                item.duration = len;
556            }
557        }
558
559        /** Dynamically switch streams **/
560        private function swap(newLevel:Number):void {
561            if (_transitionLevel == newLevel) {
562                Logger.log('Already tranisitioning to level ' + item.currentLevel + ' ; transition ignored');
563            } else {
564                _transitionLevel = newLevel;
565                                item.setLevel(newLevel);
566                Logger.log('transition to level ' + newLevel + ' initiated');
567                var nso:NetStreamPlayOptions = new NetStreamPlayOptions();
568                nso.streamName = getID(item.file);
569                nso.transition = NetStreamPlayTransitions.SWITCH;
570                                Logger.log("NetStream.play2(" + nso + ")");
571                _stream.play2(nso);
572            }
573        }
574
575        /** Set the volume level. **/
576        override public function setVolume(vol:Number):void {
577            _transformer.volume = vol / 100;
578                       
579            if (_stream) {
580                _stream.soundTransform = _transformer;
581            }
582                       
583                        super.setVolume(vol);
584        }
585               
586                /** Completes video playback **/
587                override protected function complete():void {
588                        stop();
589                        sendMediaEvent(MediaEvent.JWPLAYER_MEDIA_COMPLETE);
590                        setState(PlayerState.IDLE);
591                }
592               
593                /** Determines if the stream is a live stream **/
594                protected function get isLivestream():Boolean {
595                        // We assume it's a livestream until we hear otherwise.
596                        return (!(duration > 0) && _stream && _stream.bufferLength > 0);
597                }
598               
599                protected function get isDVR():Boolean {
600                        return Boolean(getConfigProperty('dvr'));
601                }
602               
603                protected function get duration():Number {
604                        if (isDVR) {
605                                return _dvrTotalDuration > 0 ? _dvrTotalDuration : item.duration;
606                        } else {
607                                return item.duration;
608                        }
609                }
610               
611    }
612}
Note: See TracBrowser for help on using the repository browser.