source: trunk/as3/com/jeroenwijering/models/HTTPModel.as @ 387

Revision 387, 9.9 KB checked in by jeroen, 4 years ago (diff)

added FMS3.5 dynamic streaming to RTMPModel

  • Property svn:executable set to *
Line 
1/**
2* Manages playback of http streaming flv.
3**/
4package com.jeroenwijering.models {
5
6
7import com.jeroenwijering.events.*;
8import com.jeroenwijering.models.AbstractModel;
9import com.jeroenwijering.player.Model;
10import com.jeroenwijering.utils.*;
11
12import flash.events.*;
13import flash.media.*;
14import flash.net.*;
15import flash.utils.*;
16
17
18public class HTTPModel extends AbstractModel {
19
20
21        /** Offset in bytes of the last seek. **/
22        private var byteoffset:Number = 0;
23        /** Save if the bandwidth checkin already occurs. **/
24        private var bwcheck:Boolean;
25        /** Bandwidth interval checking ID. **/
26        private var bwtimeout:Number;
27        /** Switch on startup if the bandwidth is not enough. **/
28        private var bwswitch:Boolean = true;
29        /** NetConnection object for setup of the video stream. **/
30        private var connection:NetConnection;
31        /** ID for the position interval. **/
32        private var interval:Number;
33        /** Object with keyframe times and positions. **/
34        private var keyframes:Object;
35        /** Interval ID for the loading. **/
36        private var loadinterval:Number;
37        /** Save whether metadata has already been sent. **/
38        private var meta:Boolean;
39        /** Boolean for mp4 / flv streaming. **/
40        private var mp4:Boolean;
41        /** Start parameter. **/
42        private var startparam:String = 'start';
43        /** NetStream instance that handles the stream IO. **/
44        private var stream:NetStream;
45        /** Offset in seconds of the last seek. **/
46        private var timeoffset:Number = 0;
47        /** Sound control object. **/
48        private var transformer:SoundTransform;
49        /** Video object to be instantiated. **/
50        private var video:Video;
51
52
53        /** Constructor; sets up the connection and display. **/
54        public function HTTPModel(mod:Model):void {
55                super(mod);
56                connection = new NetConnection();
57                connection.connect(null);
58                stream = new NetStream(connection);
59                stream.checkPolicyFile = true;
60                stream.addEventListener(NetStatusEvent.NET_STATUS,statusHandler);
61                stream.addEventListener(IOErrorEvent.IO_ERROR,errorHandler);
62                stream.addEventListener(AsyncErrorEvent.ASYNC_ERROR,errorHandler);
63                stream.bufferTime = model.config['bufferlength'];
64                stream.client = new NetClient(this);
65                transformer = new SoundTransform();
66                video = new Video(320,240);
67                video.smoothing = model.config['smoothing'];
68                video.attachNetStream(stream);
69                addChild(video);
70        };
71
72
73        /** Convert seekpoints to keyframes. **/
74        private function convertSeekpoints(dat:Object):Object {
75                var kfr:Object = new Object();
76                kfr.times = new Array();
77                kfr.filepositions = new Array();
78                for (var j in dat) {
79                        kfr.times[j] = Number(dat[j]['time']);
80                        kfr.filepositions[j] = Number(dat[j]['offset']);
81                }
82                return kfr;
83        };
84
85
86        /** Catch security errors. **/
87        private function errorHandler(evt:ErrorEvent):void {
88                stop();
89                model.sendEvent(ModelEvent.ERROR,{message:evt.text});
90        };
91
92
93        /** Bandwidth is checked every four seconds as long as there's loading. **/
94        private function getBandwidth(old:Number):void {
95                var ldd:Number = stream.bytesLoaded;
96                var bdw:Number = Math.round((ldd-old)*4/1000);
97                if(ldd < stream.bytesTotal) {
98                        if(bdw > 0) { model.config['bandwidth'] = bdw; }
99                        if(bwswitch) {
100                                bwswitch = false;
101                                if(item['levels'] && getLevel() != model.config['level']) {
102                                        byteoffset = -1;
103                                        seek(position);
104                                        return;
105                                }
106                        }
107                        bwtimeout = setTimeout(getBandwidth,2000,ldd);
108                }
109        };
110
111
112        /** Return which level best fits the display width and connection bandwidth. **/
113        private function getLevel():Number {
114                var lvl:Number = item['levels'].length-1;
115                for (var i:Number=0; i<item['levels'].length; i++) {
116                        if(model.config['width'] >= item['levels'][i].width &&
117                                model.config['bandwidth'] >= item['levels'][i].bitrate) {
118                                lvl = i;
119                                break;
120                        }
121                }
122                return lvl;
123        };
124
125
126        /** Return a keyframe byteoffset or timeoffset. **/
127        private function getOffset(pos:Number,tme:Boolean=false):Number {
128                if(!keyframes) {
129                        return 0;
130                }
131                for (var i:Number=0; i < keyframes.times.length - 1; i++) {
132                        if(keyframes.times[i] <= pos && keyframes.times[i+1] >= pos) {
133                                break;
134                        }
135                }
136                if(tme == true) {
137                        return keyframes.times[i];
138                } else {
139                        return keyframes.filepositions[i];
140                }
141        };
142
143
144        /** Create the video request URL. **/
145        private function getURL():String {
146                var url:String = item['file'];
147                var off:Number  = byteoffset;
148                if(model.config['http.startparam']) {
149                        startparam = model.config['http.startparam'];
150                }
151                if(item['streamer']) {
152                        if(item['streamer'].indexOf('/') > 0) {
153                                url = item['streamer'];
154                                url = getURLConcat(url,'file',item['file']);
155                        } else {
156                                startparam = item['streamer'];
157                        }
158                }
159                if(mp4) {
160                        off = timeoffset;
161                } else if (startparam == 'starttime') {
162                        startparam = 'start';
163                }
164                if(off > 0) {
165                        url = getURLConcat(url,startparam,off);
166                }
167                if(model.config['token']) {
168                        url = getURLConcat(url,'token',model.config['token']);
169                }
170                return url;
171        };
172
173
174        /** Concatenate a parameter to the url. **/
175        private function getURLConcat(url:String,prm:String,val:*):String {
176                if(url.indexOf('?') > -1) {
177                        return url+'&'+prm+'='+val;
178                } else {
179                        return url + '?'+prm+'='+val;
180                }
181        };
182
183
184        /** Load content. **/
185        override public function load(itm:Object):void {
186                item = itm;
187                position = timeoffset;
188                bwcheck = false;
189                if(item['levels']) {
190                        model.config['level'] = getLevel();
191                        item['file'] = item['levels'][model.config['level']].url;
192                }
193                stream.play(getURL());
194                clearInterval(interval);
195                interval = setInterval(positionInterval,100);
196                clearInterval(loadinterval);
197                loadinterval = setInterval(loadHandler,200);
198                clearTimeout(bwtimeout);
199                model.config['mute'] == true ? volume(0): volume(model.config['volume']);
200                model.sendEvent(ModelEvent.STATE,{newstate:ModelStates.BUFFERING});
201        };
202
203
204        /** Interval for the loading progress **/
205        private function loadHandler():void {
206                var ldd:Number = stream.bytesLoaded;
207                var ttl:Number = stream.bytesTotal;
208                var pct:Number = timeoffset/(item['duration']+0.001);
209                var off:Number = Math.round(ttl*pct/(1-pct));
210                ttl += off;
211                model.sendEvent(ModelEvent.LOADED,{loaded:ldd,total:ttl,offset:off});
212                if(ldd+off >= ttl && ldd > 0) {
213                        clearInterval(loadinterval);
214                }
215                if(ldd > 0 && !bwcheck) {
216                        bwcheck = true;
217                        bwtimeout = setTimeout(getBandwidth,2000,ldd);
218                }
219        };
220
221
222        /** Get metadata information from netstream class. **/
223        public function onClientData(dat:Object):void {
224                if(dat.width) {
225                        video.width = dat.width;
226                        video.height = dat.height;
227                        super.resize();
228                }
229                if(!item['duration'] && dat.duration) {
230                        item['duration'] = dat.duration;
231                }
232                if(dat['type'] == 'metadata' && !meta) {
233                        meta = true;
234                        if(dat.seekpoints) {
235                                mp4 = true;
236                                keyframes = convertSeekpoints(dat.seekpoints);
237                        } else {
238                                mp4 = false;
239                                keyframes = dat.keyframes;
240                        }
241                        if(item['start'] > 0) {
242                                seek(item['start']);
243                        }
244                }
245                model.sendEvent(ModelEvent.META,dat);
246        };
247
248
249        /** Pause playback. **/
250        override public function pause():void {
251                stream.pause();
252                clearInterval(interval);
253                model.sendEvent(ModelEvent.STATE,{newstate:ModelStates.PAUSED});
254        };
255
256
257        /** Resume playing. **/
258        override public function play():void {
259                stream.resume();
260                interval = setInterval(positionInterval,100);
261                model.sendEvent(ModelEvent.STATE,{newstate:ModelStates.PLAYING});
262        };
263
264
265        /** Interval for the position progress **/
266        private function positionInterval():void {
267                var pos:Number = Math.round(stream.time*10)/10;
268                if(pos > position - timeoffset + 5) {
269                        pos = position - timeoffset + 0.1;
270                }
271                if (mp4) {
272                        pos += timeoffset;
273                }
274                var bfr:Number = stream.bufferLength/stream.bufferTime;
275                if(bfr < 0.5 && pos < item['duration']-5 && model.config['state'] != ModelStates.BUFFERING) {
276                        model.sendEvent(ModelEvent.STATE,{newstate:ModelStates.BUFFERING});
277                } else if (bfr > 1 && model.config['state'] != ModelStates.PLAYING) {
278                        model.sendEvent(ModelEvent.STATE,{newstate:ModelStates.PLAYING});
279                }
280                if(pos < item['duration'] + 10) {
281                        if(pos != position) {
282                                model.sendEvent(ModelEvent.TIME,{position:pos,duration:item['duration']});
283                                position = pos;
284                        }
285                } else if (item['duration'] > 0) {
286                        stream.pause();
287                        clearInterval(interval);
288                        model.sendEvent(ModelEvent.STATE,{newstate:ModelStates.COMPLETED});
289                }
290        };
291
292
293        /** The stage has been resize. **/
294        override public function resize():void {
295                super.resize();
296                if(item['levels'] && getLevel() != model.config['level']) {
297                        byteoffset = getOffset(position);
298                        timeoffset = position = getOffset(position,true);
299                        load(item);
300                }
301        };
302
303
304        /** Seek to a specific second. **/
305        override public function seek(pos:Number):void {
306                var off:Number = getOffset(pos);
307                clearInterval(interval);
308                if(off < byteoffset || off >= byteoffset+stream.bytesLoaded) {
309                        timeoffset = position = getOffset(pos,true);
310                        byteoffset = off;
311                        load(item);
312                } else {
313                        if(model.config['state'] == ModelStates.PAUSED) {
314                                stream.resume();
315                        }
316                        position = pos;
317                        if(mp4) {
318                                stream.seek(getOffset(position-timeoffset,true));
319                        } else {
320                                stream.seek(getOffset(position,true));
321                        }
322                        play();
323                }
324        };
325
326
327        /** Receive NetStream status updates. **/
328        private function statusHandler(evt:NetStatusEvent):void {
329                switch (evt.info.code) {
330                        case "NetStream.Play.Stop":
331                                if(model.config['state'] != ModelStates.COMPLETED &&
332                                        model.config['state'] != ModelStates.BUFFERING) {
333                                        clearInterval(interval);
334                                        model.sendEvent(ModelEvent.STATE,{newstate:ModelStates.COMPLETED});
335                                }
336                                break;
337                        case "NetStream.Play.StreamNotFound":
338                                stop();
339                                model.sendEvent(ModelEvent.ERROR,{message:'Video not found: '+item['file']});
340                                break;
341                }
342                model.sendEvent(ModelEvent.META,{info:evt.info.code});
343        };
344
345
346        /** Destroy the HTTP stream. **/
347        override public function stop():void {
348                if(stream.bytesLoaded < stream.bytesTotal) {
349                        stream.close();
350                } else {
351                        stream.pause();
352                }
353                clearInterval(interval);
354                clearInterval(loadinterval);
355                byteoffset = timeoffset = position = 0;
356                keyframes = undefined;
357                meta = false;
358                model.sendEvent(ModelEvent.STATE,{newstate:ModelStates.IDLE});
359        };
360
361
362        /** Set the volume. **/
363        override public function volume(vol:Number):void {
364                transformer.volume = vol/100;
365                stream.soundTransform = transformer;
366        };
367
368
369};
370
371
372}
Note: See TracBrowser for help on using the repository browser.