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

Revision 916, 22.4 KB checked in by jeroen, 3 years ago (diff)

small addition to [915]: single, disparate transition requests do not trigger a transition.

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