├── VERSION ├── .travis.yml ├── dist ├── Player.swf └── locomote.min.js ├── .github ├── pull_request_template.md └── ISSUE_TEMPLATE │ └── bug_report.md ├── .gitmodules ├── .gitignore ├── .npmignore ├── src ├── com │ └── axis │ │ ├── mjpegclient │ │ ├── FrameEvent.as │ │ ├── Image.as │ │ ├── MJPEGClient.as │ │ ├── MJPEG.as │ │ └── Handle.as │ │ ├── rtspclient │ │ ├── IRTSPHandle.as │ │ ├── APCMA.as │ │ ├── GUID.as │ │ ├── AACFrame.as │ │ ├── PCMAFrame.as │ │ ├── FLVTag.as │ │ ├── FLVSync.as │ │ ├── NALU.as │ │ ├── ANALU.as │ │ ├── AAAC.as │ │ ├── RTPTiming.as │ │ ├── BitArray.as │ │ ├── RTP.as │ │ ├── RTSPoverTCPHandle.as │ │ ├── ByteArrayUtils.as │ │ ├── RTSPoverHTTPAPHandle.as │ │ ├── RTSPoverHTTPHandle.as │ │ ├── SDP.as │ │ └── FLVMux.as │ │ ├── Logger.as │ │ ├── ClientEvent.as │ │ ├── audioclient │ │ ├── IAudioClient.as │ │ └── AxisTransmit.as │ │ ├── codec │ │ └── g711.as │ │ ├── http │ │ ├── url.as │ │ ├── request.as │ │ └── auth.as │ │ ├── IClient.as │ │ ├── rtmpclient │ │ └── RTMPClient.as │ │ ├── httpclient │ │ └── HTTPClient.as │ │ ├── NetStreamClient.as │ │ └── ErrorManager.as └── Player.as ├── bower.json ├── package.json ├── LICENSE ├── .jscsrc ├── .jshintrc ├── gulpfile.js ├── jslib └── locomote.js └── README.md /VERSION: -------------------------------------------------------------------------------- 1 | 1.1.12 -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "0.10" 4 | -------------------------------------------------------------------------------- /dist/Player.swf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AxisCommunications/locomote-video-player/HEAD/dist/Player.swf -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | ## THIS PROJECT IS NO LONGER MAINTAINED 2 | Pull requests will not be considered 3 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "ext/as3corelib"] 2 | path = ext/as3corelib 3 | url = https://github.com/mikechambers/as3corelib.git 4 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | ## THIS PROJECT IS NO LONGER MAINTAINED 11 | Bug reports will not be considered 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | .swp 3 | .swo 4 | tags 5 | 6 | # Build and Release Folders 7 | bin/ 8 | bin-debug/ 9 | bin-release/ 10 | 11 | # Other files and folders 12 | .settings/ 13 | html-assets/ 14 | html-template/ 15 | libs/ 16 | .actionScriptProperties 17 | .flexProperties 18 | .project 19 | .DS_Store 20 | 21 | locomote.sublime-workspace 22 | locomote.sublime-project 23 | .atom-build.json 24 | node_modules 25 | nbproject 26 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | target 2 | .swp 3 | .swo 4 | tags 5 | 6 | # Build and Release Folders 7 | bin/ 8 | bin-debug/ 9 | bin-release/ 10 | 11 | # Other files and folders 12 | .settings/ 13 | html-assets/ 14 | html-template/ 15 | libs/ 16 | .actionScriptProperties 17 | .flexProperties 18 | .project 19 | .DS_Store 20 | 21 | locomote.sublime-workspace 22 | locomote.sublime-project 23 | .atom-build.json 24 | node_modules 25 | nbproject 26 | -------------------------------------------------------------------------------- /src/com/axis/mjpegclient/FrameEvent.as: -------------------------------------------------------------------------------- 1 | package com.axis.mjpegclient { 2 | import flash.display.Bitmap; 3 | import flash.events.Event; 4 | 5 | public class FrameEvent extends Event { 6 | private var frame:Bitmap; 7 | public function FrameEvent(frame:Bitmap) { 8 | super("frame"); 9 | this.frame = frame; 10 | } 11 | 12 | public function getFrame():Bitmap { 13 | return this.frame; 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/com/axis/mjpegclient/Image.as: -------------------------------------------------------------------------------- 1 | package com.axis.mjpegclient { 2 | import flash.utils.ByteArray; 3 | import flash.events.Event; 4 | 5 | public class Image extends Event { 6 | 7 | public static const NEW_IMAGE_EVENT:String = "image"; 8 | 9 | public var data:ByteArray; 10 | public var timestamp:Number; 11 | 12 | public function Image(data:ByteArray, timestamp:Number) { 13 | super(NEW_IMAGE_EVENT); 14 | this.data = data; 15 | this.timestamp = timestamp; 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/com/axis/rtspclient/IRTSPHandle.as: -------------------------------------------------------------------------------- 1 | package com.axis.rtspclient { 2 | import flash.events.IEventDispatcher; 3 | import flash.net.Socket; 4 | import flash.utils.ByteArray; 5 | 6 | public interface IRTSPHandle extends IEventDispatcher { 7 | function writeUTFBytes(value:String):void; 8 | function readBytes(bytes:ByteArray, offset:uint = 0, length:uint = 0):void; 9 | function connect():void; 10 | function reconnect():void; 11 | function disconnect():void; 12 | function cmdReceived():void; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/com/axis/Logger.as: -------------------------------------------------------------------------------- 1 | package com.axis { 2 | import flash.external.ExternalInterface; 3 | 4 | public class Logger { 5 | public static const STREAM_ERRORS:String = ""; 6 | 7 | public static function log(... args):void { 8 | if (Player.config.debugLogger) { 9 | trace.apply(null, args); 10 | } 11 | 12 | var functionName:String = "LocomoteMap['" + Player.locomoteID + "'].__playerEvent"; 13 | args.unshift(functionName, 'log'); 14 | ExternalInterface.call.apply(ExternalInterface, args); 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/com/axis/rtspclient/APCMA.as: -------------------------------------------------------------------------------- 1 | package com.axis.rtspclient { 2 | import com.axis.rtspclient.ByteArrayUtils; 3 | import com.axis.rtspclient.RTP; 4 | 5 | import flash.events.Event; 6 | import flash.events.EventDispatcher; 7 | import flash.external.ExternalInterface; 8 | import flash.utils.ByteArray; 9 | 10 | /* Assembler of PCMA frames */ 11 | public class APCMA extends EventDispatcher { 12 | private var sdp:SDP; 13 | 14 | public function APCMA() {} 15 | 16 | public function onRTPPacket(pkt:RTP):void { 17 | dispatchEvent(new PCMAFrame(pkt.getPayload(), pkt.getTimestampMS())); 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/com/axis/ClientEvent.as: -------------------------------------------------------------------------------- 1 | package com.axis { 2 | import flash.events.Event; 3 | 4 | public class ClientEvent extends Event { 5 | public static const STOPPED:String = "stopped"; 6 | public static const START_PLAY:String = "playing"; 7 | public static const FRAME:String = "frame"; 8 | public static const PAUSED:String = "paused"; 9 | public static const META:String = "meta"; 10 | 11 | public var data:Object; 12 | 13 | public function ClientEvent(type:String, data:Object = null, bubbles:Boolean = false, cancelable:Boolean = false) { 14 | super(type, bubbles, cancelable); 15 | this.data = data; 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/com/axis/rtspclient/GUID.as: -------------------------------------------------------------------------------- 1 | package com.axis.rtspclient { 2 | public class GUID { 3 | public static function create():String { 4 | var uid:Array = new Array(); 5 | var chars:Array = new Array(48, 49, 50, 51, 52, 53, 54, 55, 6 | 56, 57, 65, 66, 67, 68, 69, 70); 7 | var separator:uint = 45; 8 | var template:Array = new Array(8, 4, 4, 4, 12); 9 | 10 | for (var a:uint = 0; a < template.length; a++) { 11 | for (var b:uint = 0; b < template[a]; b++) { 12 | uid.push(chars[Math.floor(Math.random() * chars.length)]); 13 | } if (a < template.length - 1) { 14 | uid.push(separator); 15 | } 16 | } 17 | 18 | return String.fromCharCode.apply(null, uid); 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "locomote", 3 | "version": "1.1.12", 4 | "main": "dist/locomote.min.js", 5 | "authors": [ 6 | "Alexander Olsson " 7 | ], 8 | "description": "Media player in Adobe Flash with RTSP, RTMP and HTTP support", 9 | "moduleType": [ 10 | "amd", 11 | "globals" 12 | ], 13 | "keywords": [ 14 | "media", 15 | "rtsp", 16 | "rtmp", 17 | "http", 18 | "progressive", 19 | "h264" 20 | ], 21 | "license": "BSD-3-Clause", 22 | "homepage": "https://github.com/AxisCommunications/locomote-video-player", 23 | "ignore": [ 24 | "ext", 25 | "gulpfile.js", 26 | "jslib", 27 | "node_modules", 28 | "package.json", 29 | "src", 30 | ".gitignore", 31 | ".gitmodules", 32 | ".jscsrc", 33 | ".jshintrc", 34 | ".travis.yml" 35 | ] 36 | } 37 | -------------------------------------------------------------------------------- /src/com/axis/rtspclient/AACFrame.as: -------------------------------------------------------------------------------- 1 | package com.axis.rtspclient { 2 | import com.axis.rtspclient.ByteArrayUtils; 3 | import com.axis.rtspclient.RTP; 4 | 5 | import flash.events.Event; 6 | import flash.events.EventDispatcher; 7 | import flash.external.ExternalInterface; 8 | import flash.utils.ByteArray; 9 | 10 | /* Assembler of AAC frames */ 11 | public class AACFrame extends Event { 12 | public static const NEW_FRAME:String = "NEW_FRAME"; 13 | 14 | private var data:ByteArray; 15 | public var timestamp:uint; 16 | 17 | public function AACFrame(data:ByteArray, timestamp:uint) { 18 | super(AACFrame.NEW_FRAME); 19 | this.data = data; 20 | this.timestamp = timestamp; 21 | } 22 | 23 | public function writeStream(output:ByteArray):void { 24 | output.writeBytes(data, data.position); 25 | } 26 | 27 | public function getPayload():ByteArray { 28 | return data; 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/com/axis/rtspclient/PCMAFrame.as: -------------------------------------------------------------------------------- 1 | package com.axis.rtspclient { 2 | import com.axis.rtspclient.ByteArrayUtils; 3 | import com.axis.rtspclient.RTP; 4 | 5 | import flash.events.Event; 6 | import flash.events.EventDispatcher; 7 | import flash.external.ExternalInterface; 8 | import flash.utils.ByteArray; 9 | 10 | /* Assembler of PCMA frames */ 11 | public class PCMAFrame extends Event { 12 | public static const NEW_FRAME:String = "NEW_FRAME"; 13 | 14 | private var data:ByteArray; 15 | public var timestamp:uint; 16 | 17 | public function PCMAFrame(data:ByteArray, timestamp:uint) { 18 | super(PCMAFrame.NEW_FRAME); 19 | this.data = data; 20 | this.timestamp = timestamp; 21 | } 22 | 23 | public function writeStream(output:ByteArray):void { 24 | output.writeBytes(data, data.position); 25 | } 26 | 27 | public function getPayload():ByteArray { 28 | return data; 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/com/axis/audioclient/IAudioClient.as: -------------------------------------------------------------------------------- 1 | package com.axis.audioclient { 2 | /** 3 | * The interface to implement for an audio client to be used by Player. 4 | */ 5 | public interface IAudioClient { 6 | /** 7 | * Getter for the microphone volume. 8 | */ 9 | function get microphoneVolume():Number; 10 | 11 | /** 12 | * Setter for the microphone volume. 13 | * The API expects the volume to be normalized to 14 | * values between 0-100. 15 | */ 16 | function set microphoneVolume(volume:Number):void; 17 | 18 | /** 19 | * Called when the microphone should be muted. 20 | * The orignal volume should be saved so that it can 21 | * be restored when the microphone is unmuted. 22 | */ 23 | function muteMicrophone():void; 24 | 25 | /** 26 | * Called when the microphone should be unmuted. 27 | * When unmuting the volume should be reset to the 28 | * original value before muting. 29 | */ 30 | function unmuteMicrophone():void; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "locomote-video-player", 3 | "version": "1.1.12", 4 | "description": "Media player for Adobe Flash with support for RTSP, RTMP and HTTP.", 5 | "scripts": { 6 | "test": "./node_modules/.bin/gulp test" 7 | }, 8 | "repository": { 9 | "type": "git", 10 | "url": "https://github.com/AxisCommunications/locomote-video-player.git" 11 | }, 12 | "main": "dist/locomote.min.js", 13 | "devDependencies": { 14 | "jscs": "^1.8.0", 15 | "jshint": "^2.5.10", 16 | "flex-sdk": "^4.6.0-0", 17 | "underscore": "^1.7.0", 18 | "gulp": "^3.8.10", 19 | "gulp-util": "^3.0.1", 20 | "gulp-rimraf": "^0.1.1", 21 | "gulp-jshint": "^1.9.0", 22 | "gulp-jscs": "^1.3.1", 23 | "gulp-uglify": "^1.0.1", 24 | "gulp-rename": "^1.2.0", 25 | "gulp-git": "^0.5.5", 26 | "gulp-bump": "^0.1.11", 27 | "yargs": "^1.3.3" 28 | }, 29 | "engines": { 30 | "node": "0.10.x" 31 | }, 32 | "author": "Alexander Olsson ", 33 | "license": "BSD-3-Clause" 34 | } 35 | -------------------------------------------------------------------------------- /src/com/axis/rtspclient/FLVTag.as: -------------------------------------------------------------------------------- 1 | package com.axis.rtspclient { 2 | import flash.events.Event; 3 | import flash.external.ExternalInterface; 4 | import flash.utils.ByteArray; 5 | 6 | public class FLVTag extends Event { 7 | public static const NEW_FLV_TAG:String = "newFlvTag"; 8 | 9 | 10 | public var data:ByteArray; 11 | public var audio:Boolean; 12 | public var timestamp:uint; 13 | public var duration:uint; 14 | 15 | public function FLVTag(data:ByteArray, timestamp:uint, duration:uint, audio:Boolean) { 16 | super(NEW_FLV_TAG); 17 | 18 | data.position = 0; 19 | this.data = data; 20 | this.timestamp = timestamp; 21 | this.audio = audio; 22 | this.duration = duration; 23 | } 24 | 25 | public function copy():FLVTag { 26 | var newData:ByteArray = new ByteArray(); 27 | var tmpPos:uint = data.position; 28 | data.position = 0; 29 | data.readBytes(newData); 30 | newData.position = 0; 31 | data.position = tmpPos; 32 | return new FLVTag(newData, this.timestamp, this.duration, this.audio); 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/com/axis/rtspclient/FLVSync.as: -------------------------------------------------------------------------------- 1 | package com.axis.rtspclient { 2 | import flash.events.EventDispatcher; 3 | import com.axis.Logger; 4 | 5 | public class FLVSync extends EventDispatcher { 6 | private var videoTags:Array = []; 7 | private var lastAudioTimestamp:uint = 0; 8 | /** 9 | * Let audio tags dictate the sync. 10 | * All videotags with a timestamp less than or equal to the audio 11 | * tag timestamp will be dispacted. 12 | * Video tags are buffered until there is an audio tag available. 13 | */ 14 | public function onFlvTag(tag:FLVTag):void { 15 | if (tag.audio) { 16 | while (videoTags.length > 0 && tag.timestamp >= videoTags[0].timestamp) { 17 | dispatchEvent(videoTags.shift()); 18 | } 19 | 20 | dispatchEvent(tag.copy()) 21 | 22 | while (videoTags.length > 0 && tag.timestamp + tag.duration > videoTags[0].timestamp) { 23 | dispatchEvent(videoTags.shift()); 24 | } 25 | 26 | this.lastAudioTimestamp = tag.timestamp + tag.duration; 27 | } else if (tag.timestamp < this.lastAudioTimestamp) { 28 | dispatchEvent(tag.copy()) 29 | } else { 30 | videoTags.push(tag.copy()); 31 | } 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013-2016, Axis Communications AB 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, 5 | are permitted provided that the following conditions are met: 6 | 7 | 1. Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | 2. Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation and/or 12 | other materials provided with the distribution. 13 | 14 | 3. Neither the name of the copyright holder nor the names of its contributors 15 | may be used to endorse or promote products derived from this software without 16 | specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 19 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 20 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR 22 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 23 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 24 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 25 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 27 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /src/com/axis/rtspclient/NALU.as: -------------------------------------------------------------------------------- 1 | package com.axis.rtspclient { 2 | import flash.events.Event; 3 | import flash.external.ExternalInterface; 4 | import flash.utils.ByteArray; 5 | 6 | public class NALU extends Event { 7 | public static const NEW_NALU:String = "NEW_NALU"; 8 | 9 | private var data:ByteArray; 10 | public var ntype:uint; 11 | public var nri:uint; 12 | public var timestamp:uint; 13 | public var bodySize:uint; 14 | 15 | public function NALU(ntype:uint, nri:uint, data:ByteArray, timestamp:uint) { 16 | super(NEW_NALU); 17 | 18 | this.data = data; 19 | this.ntype = ntype; 20 | this.nri = nri; 21 | this.timestamp = timestamp; 22 | this.bodySize = data.bytesAvailable; 23 | } 24 | 25 | public function appendData(idata:ByteArray):void { 26 | ByteArrayUtils.appendByteArray(data, idata); 27 | this.bodySize = data.bytesAvailable; 28 | } 29 | 30 | public function isIDR():Boolean { 31 | return (5 === ntype); 32 | } 33 | 34 | public function writeSize():uint { 35 | return 2 + 2 + 1 + data.bytesAvailable; 36 | } 37 | 38 | public function writeStream(output:ByteArray):void { 39 | output.writeUnsignedInt(data.bytesAvailable + 1); // NALU length + header 40 | output.writeByte((0x0 & 0x80) | (nri & 0x60) | (ntype & 0x1F)); // NAL header 41 | output.writeBytes(data, data.position); 42 | } 43 | 44 | public function getPayload():ByteArray { 45 | var payload:ByteArray = new ByteArray(); 46 | data.position -= 1; 47 | data.readBytes(payload, 0, data.bytesAvailable); 48 | return payload; 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /.jscsrc: -------------------------------------------------------------------------------- 1 | { 2 | "requireCurlyBraces": [ "if", "else", "for", "while", "do", "try", "catch" ], 3 | "requireSpaceAfterKeywords": [ "if", "else", "for", "while", "do", "switch", "return", "try", "catch" ], 4 | "requireParenthesesAroundIIFE": true, 5 | "requireSpacesInFunctionExpression": { "beforeOpeningCurlyBrace": true }, 6 | "disallowEmptyBlocks": true, 7 | "requireSpacesInsideObjectBrackets": "all", 8 | "disallowSpaceAfterObjectKeys": true, 9 | "requireCommaBeforeLineBreak": true, 10 | "requireOperatorBeforeLineBreak": [ "?", "/", "*", "=", "==", "===", "!=", "!==", ">", ">=", "<", "<=" ], 11 | "requireSpaceBeforeBinaryOperators": [ "?", "/", "*", "=", "==", "===", "!=", "+=", "-=", "*=", "/=", "!==", ">", ">=", 12 | "<", "<=", "&&", "||", "&", "|", "^", "%" ], 13 | "disallowSpaceAfterBinaryOperators": [ "!" ], 14 | "requireSpaceAfterBinaryOperators": [ "?", "/", "*", "=", "==", "===", "!=", "+=", "-=", "*=", "/=", "!==", ">", ">=", 15 | "<", "<=", "&&", "||", "&", "|", "^", "%", ":" ], 16 | "disallowSpaceBeforeBinaryOperators": [ "," ], 17 | "disallowSpaceAfterPrefixUnaryOperators": [ "++", "--", "+", "-", "~", "!" ], 18 | "disallowSpaceBeforePostfixUnaryOperators": [ "++", "--" ], 19 | "requireSpaceBeforeBinaryOperators": [ "+", "-", "/", "*", "=", "==", "===", "!=", "!==" ], 20 | "requireSpaceAfterBinaryOperators": [ "+", "-", "/", "*", "=", "==", "===", "!=", "!==" ], 21 | "disallowKeywords": [ "with", "eval" ], 22 | "disallowMultipleLineStrings": true, 23 | "disallowMultipleLineBreaks": true, 24 | "validateLineBreaks": "LF", 25 | "disallowMixedSpacesAndTabs": true, 26 | "disallowKeywordsOnNewLine": [ "else" ], 27 | "requireLineFeedAtFileEnd": true, 28 | "maximumLineLength": 120, 29 | "excludeFiles": [ 30 | "node_modules/**", 31 | ".git/**" 32 | ] 33 | } 34 | -------------------------------------------------------------------------------- /src/com/axis/codec/g711.as: -------------------------------------------------------------------------------- 1 | package com.axis.codec { 2 | public class g711 { 3 | 4 | private static const exponentLookup:Array = [ 5 | 0, 0, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3, 3, 6 | 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 7 | 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 8 | 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 9 | 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 10 | 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 11 | 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 12 | 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 13 | 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 14 | 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 15 | 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 16 | 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 17 | 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 18 | 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 19 | 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 20 | 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7 21 | ]; 22 | 23 | public static function linearToMulaw(sample:Number):uint { 24 | var bias:uint = 0x84; 25 | var clamp:uint = 32635; 26 | 27 | var short:int = sample * 0x7fff; 28 | var negative:uint = (short & (0x1 << 31)) ? 1 : 0; 29 | 30 | if (negative) { 31 | short = -short; 32 | } 33 | 34 | if (short > clamp) { 35 | short = clamp; // Clamp the value 36 | } 37 | 38 | short += bias; // u-law bias 39 | 40 | var exponent:int = exponentLookup[(short >>> 7) & 0xFF]; 41 | var mantissa:int = (short >>> (exponent + 3)) & 0x0F; 42 | var encoded:uint = ~((negative << 7) | (exponent << 4) | mantissa); 43 | 44 | if (encoded & 0xFF === 0) { 45 | encoded = 0x02; 46 | } 47 | return encoded; 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/com/axis/rtspclient/ANALU.as: -------------------------------------------------------------------------------- 1 | package com.axis.rtspclient { 2 | import com.axis.Logger; 3 | import com.axis.rtspclient.ByteArrayUtils; 4 | import com.axis.rtspclient.RTP; 5 | 6 | import flash.events.Event; 7 | import flash.events.EventDispatcher; 8 | import flash.external.ExternalInterface; 9 | import flash.utils.ByteArray; 10 | 11 | /* Assembler of NAL units */ 12 | public class ANALU extends EventDispatcher { 13 | private static const NALTYPE_FU_A:uint = 28; 14 | private static const NALTYPE_FU_B:uint = 29; 15 | 16 | private var nalu:NALU = null; 17 | 18 | public function ANALU() { 19 | } 20 | 21 | public function onRTPPacket(pkt:RTP):void { 22 | var data:ByteArray = pkt.getPayload(); 23 | 24 | var nalhdr:uint = data.readUnsignedByte(); 25 | 26 | var nri:uint = nalhdr & 0x60; 27 | var naltype:uint = nalhdr & 0x1F; 28 | 29 | if (27 >= naltype && 0 < naltype) { 30 | /* This RTP package is a single NALU, dispatch and forget, 0 is undefined */ 31 | dispatchEvent(new NALU(naltype, nri, data, pkt.getTimestampMS())); 32 | return; 33 | } 34 | 35 | if (NALTYPE_FU_A !== naltype && NALTYPE_FU_B !== naltype) { 36 | /* 30 - 31 is undefined, ignore those (RFC3984). */ 37 | Logger.log('Undefined NAL unit, type: ' + naltype); 38 | return; 39 | } 40 | 41 | var nalfrag:uint = data.readUnsignedByte(); 42 | var nfstart:uint = (nalfrag & 0x80) >>> 7; 43 | var nfend:uint = (nalfrag & 0x40) >>> 6; 44 | var nftype:uint = nalfrag & 0x1F; 45 | 46 | if (NALTYPE_FU_B === naltype) { 47 | var nfdon:uint = data.readUnsignedShort(); 48 | } 49 | 50 | if (null === nalu) { 51 | /* Create a new NAL unit from multiple fragmented NAL units */ 52 | nalu = new NALU(nftype, nri, data, pkt.getTimestampMS()); 53 | } else { 54 | /* We've already created the NAL unit, append current data */ 55 | nalu.appendData(data); 56 | } 57 | 58 | if (1 === nfend) { 59 | dispatchEvent(nalu); 60 | nalu = null; 61 | } 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/com/axis/rtspclient/AAAC.as: -------------------------------------------------------------------------------- 1 | package com.axis.rtspclient { 2 | import com.axis.rtspclient.ByteArrayUtils; 3 | import com.axis.rtspclient.RTP; 4 | 5 | import flash.events.Event; 6 | import flash.events.EventDispatcher; 7 | import flash.external.ExternalInterface; 8 | import flash.utils.ByteArray; 9 | 10 | /* Assembler of AAC frames */ 11 | public class AAAC extends EventDispatcher { 12 | private var sdp:SDP; 13 | 14 | public function AAAC(sdp:SDP) { 15 | this.sdp = sdp; 16 | } 17 | 18 | public function onRTPPacket(pkt:RTP):void { 19 | var media:Object = sdp.getMediaBlockByPayloadType(pkt.pt); 20 | var sizeLength:uint = parseInt(media.fmtp['sizelength']); 21 | var indexLength:uint = parseInt(media.fmtp['indexlength']); 22 | var indexDeltaLength:uint = parseInt(media.fmtp['indexdeltalength']); 23 | var CTSDeltaLength:uint = parseInt(media.fmtp['ctsdeltalength']); 24 | var DTSDeltaLength:uint = parseInt(media.fmtp['dtsdeltalength']); 25 | var RandomAccessIndication:uint = parseInt(media.fmtp['randomaccessindication']); 26 | var StreamStateIndication:uint = parseInt(media.fmtp['streamstateindication']); 27 | var AuxiliaryDataSizeLength:uint = parseInt(media.fmtp['auxiliarydatasizelength']); 28 | 29 | var data:ByteArray = pkt.getPayload(); 30 | 31 | var configHeaderLength:uint = 32 | sizeLength + Math.max(indexLength, indexDeltaLength) + CTSDeltaLength + DTSDeltaLength + 33 | RandomAccessIndication + StreamStateIndication + AuxiliaryDataSizeLength; 34 | 35 | if (0 !== configHeaderLength) { 36 | /* The AU header section is not empty, read it from payload */ 37 | var auHeadersLengthInBits:uint = data.readUnsignedShort(); // Always 2 octets, without padding 38 | var auHeadersLengthPadded:uint = (auHeadersLengthInBits + auHeadersLengthInBits % 8) / 8; // Add padding 39 | var auHeaders:ByteArray = new ByteArray(); 40 | data.readBytes(auHeaders, 0, auHeadersLengthPadded); 41 | 42 | /* What should we do with the headers? */ 43 | } 44 | 45 | dispatchEvent(new AACFrame(data, pkt.getTimestampMS())); 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/com/axis/rtspclient/RTPTiming.as: -------------------------------------------------------------------------------- 1 | package com.axis.rtspclient { 2 | import flash.utils.ByteArray; 3 | 4 | public class RTPTiming { 5 | 6 | public var rtpTime:Object; 7 | public var range:Object; 8 | public var live:Boolean; 9 | 10 | public function RTPTiming(rtpTime:Object, range:Object, live:Boolean) { 11 | this.rtpTime = rtpTime; 12 | this.range = range; 13 | this.live = live; 14 | } 15 | 16 | public function rtpTimeForControl(control:String):Number { 17 | /* control parameter and url in rtp-info may not equal but control 18 | * parameter is always part of the url */ 19 | for (var c:String in this.rtpTime) { 20 | if (c.indexOf(control) >= 0) { 21 | return this.rtpTime[c]; 22 | } 23 | } 24 | return 0; 25 | } 26 | 27 | public function toString():String { 28 | var res:String = 'rtpTime:'; 29 | for (var control:String in rtpTime) { 30 | res += '\n ' + control + ': ' + rtpTime[control]; 31 | } 32 | if (range) { 33 | res += '\nrange: ' + range.from + ' - ' + range.to; 34 | } 35 | res += '\nlive: ' + live; 36 | return res; 37 | } 38 | 39 | public static function parse(rtpInfo:String, range:String):RTPTiming { 40 | var rtpTime:Object = {}; 41 | for each (var track:String in rtpInfo.split(',')) { 42 | var rtpTimeMatch:Object = /^.*url=([^;]*);.*rtptime=(\d+).*$/.exec(track); 43 | rtpTime[rtpTimeMatch[1]] = parseInt(rtpTimeMatch[2]); 44 | } 45 | var rangeMatch:Object = /^npt=(.*)-(.*)$/.exec(range); 46 | var rangeFrom:String = rangeMatch[1]; 47 | var rangeTo:String = rangeMatch[2]; 48 | var from:Number = 0; 49 | var to:Number = rangeTo.length > 0 ? Math.round(parseFloat(rangeTo) * 1000) : -1; 50 | var live:Boolean = rangeFrom == 'now'; 51 | if (rangeFrom != 'now') { 52 | from = Math.round(parseFloat(rangeFrom) * 1000); 53 | /* Some idiot RTSP servers writes Range: npt=0.000-0.000 in the header... */ 54 | to = to <= from ? -1 : to; 55 | } 56 | 57 | return new RTPTiming(rtpTime, { from: from, to: to }, live); 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/com/axis/http/url.as: -------------------------------------------------------------------------------- 1 | package com.axis.http { 2 | public class url { 3 | /** 4 | * Parses an URL. 5 | * mx.utils.URLUtil is not good enough since it doesn't support 6 | * authorization. 7 | * 8 | * @param url The URL represented as a string. 9 | * @return An object with the following parameters set: 10 | * full, protocol, urlpath, user, pass, host, port. 11 | * If URL part is not in the specified url, the corresponding 12 | * value is null. 13 | */ 14 | public static function parse(url:String):Object { 15 | var ret:Object = {}; 16 | 17 | var regex:RegExp = /^(?P[^:]+):\/\/(?P[^\/]+)(?P.*)$/; 18 | var result:Array = regex.exec(url); 19 | 20 | ret.full = url; 21 | ret.protocol = result.protocol; 22 | ret.urlpath = result.urlpath; 23 | 24 | var parts:Array = result.urlpath.split('/'); 25 | ret.basename = parts.pop().split(/\?|#/)[0]; 26 | ret.basepath = parts.join('/'); 27 | 28 | var loginSplit:Array = result['login'].split('@'); 29 | var hostport:Array = loginSplit[0].split(':'); 30 | var userpass:Array = [ null, null ]; 31 | if (loginSplit.length === 2) { 32 | userpass = loginSplit[0].split(':'); 33 | hostport = loginSplit[1].split(':'); 34 | } 35 | 36 | ret.user = userpass[0]; 37 | ret.pass = userpass[1]; 38 | ret.host = hostport[0]; 39 | 40 | ret.port = (null == hostport[1]) ? protocolDefaultPort(ret.protocol) : hostport[1]; 41 | ret.portDefined = (null != hostport[1]); 42 | 43 | return ret; 44 | } 45 | 46 | public static function isAbsolute(url:String):Boolean { 47 | return /^[^:]+:\/\//.test(url); 48 | } 49 | 50 | private static function protocolDefaultPort(protocol:String):uint { 51 | switch (protocol) { 52 | case 'rtmp': return 1935; 53 | case 'rtsp': return 554; 54 | case 'rtsph': return 80; 55 | case 'rtsphs': return 443; 56 | case 'rtsphap': return 443; 57 | case 'http': return 80; 58 | case 'https': return 443; 59 | case 'httpm': return 80; 60 | } 61 | 62 | return 0; 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | // Restricting options 3 | "strict": true, // https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Functions_and_function_scope/Strict_mode 4 | "trailing": true, // Makes it an error to leave a trailing whitespace in your code. 5 | "curly": true, // Force braces around blocks in loops and conditionals. 6 | "undef": true, // Prohibits the use of explicitly undeclared variables. Use /*global ... */ for exceptions 7 | "latedef": true, // Prohibits the use of a variable before it has been defined. 8 | "unused": "vars", // Warns when you define and never use your variables. 9 | "newcap": true, // Forces you to capitalize names of constructor functions. 10 | "noarg": true, // Prohibits the use of arguments.caller and arguments.callee, which are deprecated in EcmaScript 5. 11 | "quotmark": "single", // Force single quotes for strings 12 | 13 | // Relaxing options 14 | "expr": true, // Allow expressions where normally you would expect to see assignments or function calls. 15 | "onecase": true, // Allow switches with only one case (not counting default) 16 | "sub": true, // Allow using [] notation when it can also be expressed in dot notation: person['name'] vs. person.name. 17 | 18 | // Environment 19 | "browser": true, // Define common browser globals: document, navigator, etc. 20 | "devel": true, // Define globals that are usually used for debugging: console, alert, etc. 21 | "globals": { // Define globals introduced by non-AMD compatible modules 22 | "require": true, 23 | "define": true, 24 | "describe": true, 25 | "xdescribe": true, 26 | "it": true, 27 | "xit": true, 28 | "runs": true, 29 | "waits": true, 30 | "waitsFor": true, 31 | "expect": true, 32 | "beforeEach": true, 33 | "afterEach": true, 34 | "sinon": true, 35 | "jasmine": true, 36 | "__dirname": true, 37 | "module": false 38 | }, 39 | 40 | // Misc. 41 | "white": true, // Enforce Crockford style guides 42 | "indent": 2 // Set indentation to two spaces 43 | } 44 | -------------------------------------------------------------------------------- /src/com/axis/rtspclient/BitArray.as: -------------------------------------------------------------------------------- 1 | package com.axis.rtspclient { 2 | import com.axis.ErrorManager; 3 | 4 | import flash.utils.ByteArray; 5 | 6 | public class BitArray extends ByteArray { 7 | private var src:ByteArray; 8 | private var byte:uint; 9 | private var bitpos:uint; 10 | 11 | public function BitArray(src:ByteArray) { 12 | this.src = clone(src); 13 | this.bitpos = 0; 14 | this.byte = 0; /* This should really be undefined, uint wont allow it though */ 15 | } 16 | 17 | public function readBits(length:uint):uint { 18 | if (32 < length || 0 === length) { 19 | /* To big for an uint */ 20 | ErrorManager.dispatchError(819, null, true); 21 | } 22 | 23 | var result:uint = 0; 24 | for (var i:uint = 1; i <= length; ++i) { 25 | if (0 === bitpos) { 26 | /* Previous byte all read out. Get a new one. */ 27 | byte = src.readUnsignedByte(); 28 | } 29 | 30 | /* Shift result one left to make room for another bit, 31 | then add the next bit on the stream. */ 32 | result = (result << 1) | ((byte >> (8 - (++bitpos))) & 0x01); 33 | bitpos %= 8; 34 | } 35 | 36 | return result; 37 | } 38 | 39 | public function readUnsignedExpGolomb():uint { 40 | var bitsToRead:uint = 0; 41 | while (readBits(1) !== 1) bitsToRead++; 42 | 43 | if (bitsToRead == 0) return 0; /* Easy peasy, just a single 1. This is 0 in exp golomb */ 44 | if (bitsToRead >= 31) ErrorManager.dispatchError(820, null, true); 45 | 46 | var n:uint = readBits(bitsToRead); /* Read all bits part of this number */ 47 | n |= (0x1 << (bitsToRead)); /* Move in the 1 read by while-statement above */ 48 | 49 | return n - 1; /* Because result in exp golomb is one larger */ 50 | } 51 | 52 | public function readSignedExpGolomb():uint { 53 | var r:uint = this.readUnsignedExpGolomb(); 54 | if (r & 0x01) { 55 | r = (r+1) >> 1; 56 | } else { 57 | r = -(r >> 1); 58 | } 59 | return r; 60 | } 61 | 62 | public function clone(source:ByteArray):* { 63 | var temp:ByteArray = new ByteArray(); 64 | temp.writeObject(source); 65 | temp.position = 0; 66 | return(temp.readObject()); 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/com/axis/rtspclient/RTP.as: -------------------------------------------------------------------------------- 1 | package com.axis.rtspclient { 2 | import com.axis.Logger; 3 | import com.axis.rtspclient.ByteArrayUtils; 4 | 5 | import flash.events.Event; 6 | import flash.utils.ByteArray; 7 | 8 | public class RTP extends Event { 9 | private var data:ByteArray; 10 | private var media:Object; 11 | private var timing:RTPTiming; 12 | 13 | public var version:uint; 14 | public var padding:uint; 15 | public var extension:uint; 16 | public var csrc:uint; 17 | public var ssrc:uint; 18 | public var marker:uint; 19 | public var pt:uint; 20 | public var sequence:uint; 21 | public var timestamp:uint; 22 | 23 | public var headerLength:uint; 24 | public var bodyLength:uint; 25 | 26 | public function RTP(pkt:ByteArray, sdp:SDP, timing:RTPTiming) { 27 | this.timing = timing; 28 | var line1:uint = pkt.readUnsignedInt(); 29 | 30 | version = (line1 & 0xC0000000) >>> 30; 31 | padding = (line1 & 0x20000000) >>> 29; 32 | extension = (line1 & 0x10000000) >>> 28; 33 | csrc = (line1 & 0x0F000000) >>> 24; 34 | marker = (line1 & 0x00800000) >>> 23; 35 | pt = (line1 & 0x007F0000) >>> 16; 36 | sequence = (line1 & 0x0000FFFF) >>> 0; 37 | timestamp = pkt.readUnsignedInt(); 38 | ssrc = pkt.readUnsignedInt(); 39 | 40 | headerLength = pkt.position; 41 | bodyLength = pkt.bytesAvailable; 42 | 43 | media = sdp.getMediaBlockByPayloadType(pt); 44 | if (null === media || -1 === media.fmt.indexOf(pt)) { 45 | Logger.log('Media description for payload type: ' + pt + ' not provided.'); 46 | } 47 | 48 | super(media.type.toUpperCase() + '_' + media.rtpmap[pt].name.toUpperCase() + '_PACKET', false, false); 49 | 50 | this.data = pkt; 51 | } 52 | 53 | public function getPayload():ByteArray { 54 | return data; 55 | } 56 | 57 | public function getTimestampMS():uint { 58 | return timing.range.from + (1000 * (timestamp - timing.rtpTimeForControl(media.control)) / media.rtpmap[pt].clock); 59 | } 60 | 61 | public override function toString():String { 62 | return "RTP(" + 63 | "version:" + version + ", " + 64 | "padding:" + padding + ", " + 65 | "extension:" + extension + ", " + 66 | "csrc:" + csrc + ", " + 67 | "marker:" + marker + ", " + 68 | "pt:" + pt + ", " + 69 | "sequence:" + sequence + ", " + 70 | "timestamp:" + timestamp + ", " + 71 | "ssrc:" + ssrc + ")"; 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/com/axis/rtspclient/RTSPoverTCPHandle.as: -------------------------------------------------------------------------------- 1 | package com.axis.rtspclient { 2 | import com.axis.ClientEvent; 3 | import com.axis.ErrorManager; 4 | 5 | import flash.events.Event; 6 | import flash.events.EventDispatcher; 7 | import flash.events.IOErrorEvent 8 | import flash.events.ProgressEvent; 9 | import flash.events.SecurityErrorEvent; 10 | import flash.net.Socket; 11 | import flash.utils.ByteArray; 12 | 13 | import mx.utils.Base64Encoder; 14 | 15 | public class RTSPoverTCPHandle extends EventDispatcher implements IRTSPHandle { 16 | private var channel:Socket; 17 | private var urlParsed:Object; 18 | 19 | public function RTSPoverTCPHandle(iurl:Object) { 20 | this.urlParsed = iurl; 21 | 22 | channel = new Socket(); 23 | channel.timeout = 5000; 24 | channel.addEventListener(Event.CONNECT, onConnect); 25 | channel.addEventListener(ProgressEvent.SOCKET_DATA, onData); 26 | channel.addEventListener(IOErrorEvent.IO_ERROR, onIOError); 27 | channel.addEventListener(SecurityErrorEvent.SECURITY_ERROR, onSecurityError); 28 | } 29 | 30 | public function writeUTFBytes(value:String):void { 31 | channel.writeUTFBytes(value); 32 | channel.flush(); 33 | } 34 | 35 | public function readBytes(bytes:ByteArray, offset:uint = 0, length:uint = 0):void { 36 | channel.readBytes(bytes, offset, length); 37 | } 38 | 39 | public function connect():void { 40 | channel.connect(urlParsed.host, urlParsed.port); 41 | } 42 | 43 | public function reconnect():void { 44 | channel.close(); 45 | connect(); 46 | } 47 | 48 | public function disconnect():void { 49 | try { 50 | channel.close(); 51 | } catch (error:*) { 52 | } 53 | channel = null; 54 | 55 | /* should probably wait for close, but it doesn't seem to fire properly */ 56 | dispatchEvent(new Event("closed")); 57 | } 58 | 59 | // Not applicable 60 | public function cmdReceived():void {} 61 | 62 | private function onConnect(event:Event):void { 63 | dispatchEvent(new Event("connected")); 64 | } 65 | 66 | private function onData(event:ProgressEvent):void { 67 | dispatchEvent(new Event("data")); 68 | } 69 | 70 | private function onIOError(event:IOErrorEvent):void { 71 | ErrorManager.dispatchError(732, [event.text]); 72 | dispatchEvent(new Event("closed")); 73 | } 74 | 75 | private function onSecurityError(event:SecurityErrorEvent):void { 76 | ErrorManager.dispatchError(731, [event.text]); 77 | dispatchEvent(new Event("closed")); 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /dist/locomote.min.js: -------------------------------------------------------------------------------- 1 | !function(t,e){"use strict";"object"==typeof exports?module.exports=e():"function"==typeof define&&define.amd?define(e):t.Locomote=e()}(this,function(){"use strict";function t(e,n){return e?(window.LocomoteMap||(window.LocomoteMap={}),window.LocomoteMap[e]&&window.LocomoteMap[e].swfready?window.LocomoteMap[e]:void 0===this?(window.LocomoteMap[e]=new t(e,n),window.LocomoteMap[e]):(window.LocomoteMap[e]=this,this.callbacks=[],this.swfready=!1,this.__embed(e,n),this)):null}return t.prototype={__embed:function(t,e){this.tag=t;var n=function(){function t(){return Math.floor(65536*(1+Math.random())).toString(16).substring(1)}return function(){return t()+t()+"-"+t()+"-"+t()+"-"+t()+"-"+t()+t()+t()}}(),i=n(),o='',s={width:"100%",height:"100%",allowscriptaccess:"always",wmode:"transparent",quality:"high",flashvars:"locomoteID="+t,allowFullScreenInteractive:!0,movie:e,name:t};for(var a in s)s.hasOwnProperty(a)&&(o+='');if(o+="","string"==typeof t&&document.getElementById(t))document.getElementById(t).innerHTML=o,this.e=document.getElementById(i);else{t.innerHTML=o;for(var r=t.getElementsByClassName("locomote-player"),u=0;u buffer.bytesAvailable) { 24 | /* Headers parsed fine, but full body is not here yet. */ 25 | buffer.position = 0; 26 | return false; 27 | } 28 | 29 | return parsed; 30 | } 31 | 32 | public static function parse(data:String):Object { 33 | var ret:Object = {}; 34 | 35 | var lines:Array = data.split('\r\n'); 36 | var statusRegex:RegExp = /^(?P[^\/]+)\/(?P[^ ]+) (?P[0-9]+) (?P.*)$/; 37 | var status:Array = statusRegex.exec(lines.shift()); 38 | 39 | if (status) { 40 | /* statusRegex will fail if this is multipart block (in which case these parameters are not valid) */ 41 | ret.proto = status.proto; 42 | ret.version = status.version; 43 | ret.code = uint(status.code); 44 | ret.message = status.message; 45 | } 46 | 47 | ret.headers = {}; 48 | for each (var header:String in lines) { 49 | if (header.length === 0) continue; 50 | 51 | var t:Array = header.split(':'); 52 | var key:String = t.shift().replace(/^[\s]*(.*)[\s]*$/, '$1').toLowerCase(); 53 | var val:String = t.join(':').replace(/^[\s]*(.*)[\s]*$/, '$1'); 54 | parseMiddleware(key, val, ret.headers); 55 | } 56 | 57 | return ret; 58 | } 59 | 60 | private static function parseMiddleware(key:String, val:String, hdr:Object):void { 61 | switch (key) { 62 | case 'www-authenticate': 63 | if (!hdr['www-authenticate']) 64 | hdr['www-authenticate'] = {}; 65 | 66 | if (/^basic/i.test(val)) { 67 | var basicRealm:RegExp = /realm="([^"]*)"/i; 68 | hdr['www-authenticate'].basicRealm = basicRealm.exec(val)[1]; 69 | } 70 | 71 | if (/^digest/i.test(val)) { 72 | var params:Array = val.substr(7).split(/,\s*/); 73 | for each (var p:String in params) { 74 | var kv:Array = p.split('='); 75 | if (2 !== kv.length) continue; 76 | 77 | if (kv[0].toLowerCase() === 'realm') kv[0] = 'digestRealm'; 78 | hdr['www-authenticate'][kv[0]] = kv[1].replace(/^"(.*)"$/, '$1'); 79 | } 80 | } 81 | 82 | break; 83 | default: 84 | /* In the default case, just take the value as-is */ 85 | hdr[key] = val; 86 | } 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/com/axis/rtspclient/ByteArrayUtils.as: -------------------------------------------------------------------------------- 1 | package com.axis.rtspclient { 2 | import com.axis.ErrorManager; 3 | 4 | import flash.utils.ByteArray; 5 | 6 | public class ByteArrayUtils { 7 | 8 | public static function indexOf(target:ByteArray, pattern:*, fromIndex:int = 0):int { 9 | var arr:Array, end:Boolean, found:Boolean, a:int, i:int, j:int, k:int; 10 | var toFind:ByteArray = toByteArray(pattern); 11 | if (toFind == null) { 12 | // ** type of pattern unsupported ** 13 | ErrorManager.dispatchError(821, null, true); 14 | return -1; 15 | } 16 | 17 | a = toFind.length; 18 | j = target.length - a; 19 | 20 | if (fromIndex < 0) { 21 | i = j + fromIndex; 22 | if (i < 0) { 23 | return -1; 24 | } 25 | } else { 26 | i = fromIndex; 27 | } 28 | 29 | while (!end) { 30 | if (target[i] == toFind[0]) { 31 | // ** found a possible candidate ** 32 | found = true; 33 | k = a; 34 | while (--k) { 35 | if (target[i + k] != toFind[k]) { 36 | // ** doesn't match, false candidate ** 37 | found = false; 38 | break; 39 | } 40 | } 41 | if (found) { 42 | return i; 43 | } 44 | } 45 | if (fromIndex < 0) { 46 | end = (--i < 0); 47 | } else { 48 | end = (++i > j); 49 | } 50 | } 51 | return -1; 52 | } 53 | 54 | public static function toByteArray(obj:*):ByteArray { 55 | var toFind:ByteArray; 56 | if (obj is ByteArray) { 57 | toFind = obj; 58 | } else { 59 | toFind = new ByteArray(); 60 | if (obj is Array) { 61 | // ** looking for a sequence of target ** 62 | var i:int = obj.length; 63 | while (i--) { 64 | toFind[i] = obj[i]; 65 | } 66 | } else if (obj is String) { 67 | // ** looking for a sequence of string characters ** 68 | toFind.writeUTFBytes(obj); 69 | } else { 70 | return null; 71 | } 72 | } 73 | return toFind; 74 | } 75 | 76 | public static function hexdump(ba:ByteArray, offset:uint = 0, length:int = -1):String { 77 | var result:String = ""; 78 | 79 | var realOffset:uint = offset; 80 | 81 | var realLength:int = (length === -1) ? 82 | ba.length - realOffset : 83 | Math.min(ba.length - realOffset, length); 84 | 85 | for (var i:int = realOffset; i < realOffset + realLength; i++) { 86 | result += "" + (ba[i] < 16 ? "0" : "") + ba[i].toString(16) + (((i - realOffset) % 16 == 7) ? " " : "") + (((i - realOffset) % 16 == 15) ? "\n" : " "); 87 | } 88 | return result; 89 | } 90 | 91 | public static function appendByteArray(dest:ByteArray, src:ByteArray):void { 92 | var prepos:uint = dest.position; 93 | dest.position = dest.length; 94 | dest.writeBytes(src, src.position); 95 | dest.position = prepos; 96 | } 97 | 98 | public static function createFromHexstring(hex:String):ByteArray { 99 | var res:ByteArray = new ByteArray(); 100 | for (var i:uint = 0; i < hex.length; i += 2) { 101 | res.writeByte(parseInt(hex.substr(i, 2), 16)); 102 | } 103 | 104 | return res; 105 | } 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /src/com/axis/IClient.as: -------------------------------------------------------------------------------- 1 | package com.axis { 2 | import flash.display.DisplayObject; 3 | import flash.events.IEventDispatcher; 4 | 5 | /** 6 | * The interface to implement for a client to be used by Player. 7 | * In addidtion the the methods enforced in this interface, it should 8 | * emit ClientEvent for certain actions: 9 | * 10 | * - ClientEvent.NETSTREAM_CREATED for automatic video resizing and other goodies. 11 | */ 12 | public interface IClient extends IEventDispatcher { 13 | 14 | /** 15 | * Should return the area where the video the client 16 | * produces should be shown. This must be an instance of DisplayObject. 17 | */ 18 | function getDisplayObject():DisplayObject; 19 | 20 | /** 21 | * Should return the position of the playahead, in milliseconds. 22 | * Returns -1 if unavailable 23 | */ 24 | function getCurrentTime():Number; 25 | 26 | /** 27 | * Should return the size of the playback buffer in milliseconds. 28 | * Returns -1 if unavailable 29 | */ 30 | function bufferedTime():Number; 31 | 32 | /** 33 | * Called when the client should start the stream. 34 | * Any connections should be made at this point 35 | * options include optional offset, the time in the stream to start playing 36 | * at. 37 | */ 38 | function start(options:Object):Boolean; 39 | 40 | /** 41 | * Called when the client should stop the stream. 42 | * The video/audio should stop playing at this pont 43 | * and all connections should be terminated. 44 | */ 45 | function stop():Boolean; 46 | 47 | /** 48 | * Called when the client should seek in the stream 49 | * to a given position (in sec). 50 | */ 51 | function seek(position:Number):Boolean; 52 | 53 | /** 54 | * Called when the client should pause the stream. This should 55 | * preferrably be accomplished by pausing the incomming stream, 56 | * but this may not always be possible. If that is not possible, 57 | * the client should return false and pausing will be accomplished 58 | * in the player. 59 | */ 60 | function pause():Boolean; 61 | 62 | /** 63 | * Called when the stream should be resumed. This will only be 64 | * called if the client previously claimed to have paused the 65 | * stream by returning `true` from a call to `stop`. 66 | */ 67 | function resume():Boolean; 68 | 69 | /** 70 | * Called when the client should emit an event for each frame and wait for 71 | * a call to playFrames to play the frames. 72 | * Returns false if the action is not possible. 73 | */ 74 | function setFrameByFrame(frameByFrame:Boolean):Boolean; 75 | 76 | /** 77 | * Call to play all frames with timestamp equal to or lower than the given 78 | * timestamp. 79 | * A call to this function does nothing if a previous call to 80 | * setFrameByFrame(true) has not been made. 81 | */ 82 | function playFrames(timestamp:Number):void; 83 | 84 | /** 85 | * Called when the client should buffer a certain amount seconds 86 | * before continuing playback. 87 | */ 88 | function setBuffer(seconds:Number):Boolean; 89 | 90 | /** 91 | * Called when the client should start keep alive routine 92 | */ 93 | function setKeepAlive(seconds:Number):Boolean; 94 | 95 | /** 96 | * Returns the current achieved frames per second for the client. 97 | */ 98 | function currentFPS():Number; 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /src/com/axis/http/auth.as: -------------------------------------------------------------------------------- 1 | package com.axis.http { 2 | import com.adobe.crypto.MD5; 3 | 4 | import com.axis.Logger; 5 | import com.axis.rtspclient.GUID; 6 | 7 | import flash.net.Socket; 8 | 9 | import mx.utils.Base64Encoder; 10 | 11 | public class auth { 12 | 13 | public static function basic(user:String, pass:String):String { 14 | var b64:Base64Encoder = new Base64Encoder(); 15 | b64.insertNewLines = false; 16 | b64.encode(user + ':' + pass); 17 | return 'Basic ' + b64.toString(); 18 | } 19 | 20 | public static function digest( 21 | user:String, 22 | pass:String, 23 | httpmethod:String, 24 | realm:String, 25 | uri:String, 26 | qop:String, 27 | nonce:String, 28 | nc:uint 29 | ):String { 30 | /* NOTE: Unsupported: md5-sess and auth-int */ 31 | 32 | if (qop && 'auth' !== qop) { 33 | Logger.log('unsupported quality of protection: ' + qop); 34 | return ""; 35 | } 36 | 37 | var ha1:String = MD5.hash(user + ':' + realm + ':' + pass); 38 | var ha2:String = MD5.hash(httpmethod + ':' + uri); 39 | var cnonce:String = MD5.hash(GUID.create()); 40 | 41 | var hashme:String = qop ? 42 | (ha1 + ':' + nonce + ':' + nc + ':' + cnonce + ':' + qop + ':' + ha2) : 43 | (ha1 + ':' + nonce + ':' + ha2) 44 | var resp:String = MD5.hash(hashme); 45 | 46 | return 'Digest ' + 47 | 'username="' + user + '", ' + 48 | 'realm="' + realm + '", ' + 49 | 'nonce="' + nonce + '", ' + 50 | 'uri="' + uri + '", ' + 51 | 'nc="' + nc + '", ' + 52 | (qop ? ('qop="' + qop + '", ') : '') + 53 | (qop ? ('cnonce="' + cnonce + '", ') : '') + 54 | 'response="' + resp + '"' 55 | ; 56 | } 57 | 58 | public static function nextMethod(current:String, authOpts:Object):String { 59 | switch (current) { 60 | case 'none': 61 | /* No authorization attempt yet, try with the best method supported by server */ 62 | if (authOpts.digestRealm) 63 | return 'digest'; 64 | else if (authOpts.hasOwnProperty('basicRealm')) 65 | return 'basic'; 66 | break; 67 | 68 | case 'digest': 69 | /* Weird to get unauthorized here unless credentials are invalid. 70 | On the off-chance of server-bug, try basic aswell */ 71 | if (authOpts.basicRealm) 72 | return 'basic'; 73 | 74 | case 'basic': 75 | /* If we failed with basic, we're done. Credentials are invalid. */ 76 | } 77 | 78 | /* Getting the same method as passed as current should be considered an error */ 79 | return current; 80 | } 81 | 82 | public static function authorizationHeader( 83 | method:String, 84 | authState:String, 85 | authOpts:Object, 86 | urlParsed:Object, 87 | digestNC:uint):String { 88 | 89 | var content:String = ''; 90 | switch (authState) { 91 | case "basic": 92 | content = basic(urlParsed.user, urlParsed.pass); 93 | break; 94 | 95 | case "digest": 96 | content = digest( 97 | urlParsed.user, 98 | urlParsed.pass, 99 | method, 100 | authOpts.digestRealm, 101 | urlParsed.urlpath, 102 | authOpts.qop, 103 | authOpts.nonce, 104 | digestNC 105 | ); 106 | break; 107 | 108 | default: 109 | case "none": 110 | return ""; 111 | } 112 | 113 | return "Authorization: " + content + "\r\n" 114 | } 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /src/com/axis/rtmpclient/RTMPClient.as: -------------------------------------------------------------------------------- 1 | package com.axis.rtmpclient { 2 | import com.axis.ClientEvent; 3 | import com.axis.NetStreamClient; 4 | import com.axis.ErrorManager; 5 | import com.axis.IClient; 6 | import com.axis.Logger; 7 | 8 | import flash.events.AsyncErrorEvent; 9 | import flash.events.EventDispatcher; 10 | import flash.events.IOErrorEvent; 11 | import flash.events.NetStatusEvent; 12 | import flash.events.SecurityErrorEvent; 13 | import flash.media.Video; 14 | import flash.net.NetConnection; 15 | import flash.net.NetStream; 16 | 17 | public class RTMPClient extends NetStreamClient implements IClient { 18 | private var urlParsed:Object; 19 | private var nc:NetConnection; 20 | private var streamServer:String; 21 | private var streamId:String; 22 | 23 | public function RTMPClient(urlParsed:Object) { 24 | this.urlParsed = urlParsed; 25 | } 26 | 27 | public function start(options:Object):Boolean { 28 | this.nc = new NetConnection(); 29 | this.nc.addEventListener(NetStatusEvent.NET_STATUS, onConnectionStatus); 30 | this.nc.addEventListener(AsyncErrorEvent.ASYNC_ERROR, asyncErrorHandler); 31 | this.nc.addEventListener(IOErrorEvent.IO_ERROR, onIOError); 32 | this.nc.addEventListener(SecurityErrorEvent.SECURITY_ERROR, onSecurityError); 33 | this.nc.client = this; 34 | 35 | if (urlParsed.hasOwnProperty("connect") && urlParsed.hasOwnProperty("streamName")) { 36 | this.streamId = urlParsed.streamName; 37 | this.streamServer = urlParsed.connect; 38 | } else { 39 | this.streamId = urlParsed.basename; 40 | this.streamServer = urlParsed.protocol + '://'; 41 | this.streamServer += urlParsed.host; 42 | this.streamServer += ((urlParsed.portDefined) ? (':' + urlParsed.port) : '') 43 | this.streamServer += urlParsed.basepath; 44 | } 45 | 46 | Logger.log('RTMPClient: connecting to server: \'' + streamServer + '\''); 47 | this.nc.connect(streamServer); 48 | 49 | return true; 50 | } 51 | 52 | public function stop():Boolean { 53 | this.ns.dispose(); 54 | this.nc.close(); 55 | return true; 56 | } 57 | 58 | public function seek(position:Number):Boolean { 59 | this.ns.seek(position); 60 | return true; 61 | } 62 | 63 | public function pause():Boolean { 64 | if (this.currentState !== 'playing') { 65 | ErrorManager.dispatchError(800); 66 | return false; 67 | } 68 | this.ns.pause(); 69 | return true; 70 | } 71 | 72 | public function resume():Boolean { 73 | if (this.currentState !== 'paused') { 74 | ErrorManager.dispatchError(801); 75 | return false; 76 | } 77 | this.ns.resume(); 78 | return true; 79 | } 80 | 81 | public function setFrameByFrame(frameByFrame:Boolean):Boolean { 82 | return false; 83 | } 84 | 85 | public function playFrames(timestamp:Number):void {} 86 | 87 | public function setBuffer(seconds:Number):Boolean { 88 | this.ns.bufferTime = seconds; 89 | this.ns.pause(); 90 | this.ns.resume(); 91 | return true; 92 | } 93 | 94 | public function setKeepAlive(seconds:Number):Boolean { 95 | return false; 96 | } 97 | 98 | private function onConnectionStatus(event:NetStatusEvent):void { 99 | if ('NetConnection.Connect.Success' === event.info.code) { 100 | Logger.log('RTMPClient: connected'); 101 | this.ns = new NetStream(this.nc); 102 | this.setupNetStream(); 103 | 104 | Logger.log('RTMPClient: starting stream: \'' + this.streamId + '\''); 105 | this.ns.play(this.streamId); 106 | } 107 | 108 | if ('NetConnection.Connect.Closed' === event.info.code) { 109 | this.currentState = 'stopped'; 110 | dispatchEvent(new ClientEvent(ClientEvent.STOPPED)); 111 | this.ns.dispose(); 112 | } 113 | } 114 | 115 | private function asyncErrorHandler(event:AsyncErrorEvent):void { 116 | Logger.log('RTMPClient: Async Error Event:' + event.error); 117 | } 118 | 119 | public function onBWDone(arg:*):void { 120 | /* Why is this enforced by NetConnection? */ 121 | } 122 | 123 | public function onFCSubscribe(info:Object):void { 124 | /* Why is this enforced by NetConnection? */ 125 | } 126 | 127 | private function onIOError(event:IOErrorEvent):void { 128 | ErrorManager.dispatchError(729, [event.text]); 129 | } 130 | 131 | private function onSecurityError(event:SecurityErrorEvent):void { 132 | ErrorManager.dispatchError(730, [event.text]); 133 | } 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'); 2 | var _ = require('underscore'); 3 | var gulp = require('gulp'); 4 | var gutil = require('gulp-util'); 5 | var rimraf = require('gulp-rimraf'); 6 | var jshint = require('gulp-jshint'); 7 | var jscs = require('gulp-jscs'); 8 | var rename = require('gulp-rename'); 9 | var uglify = require('gulp-uglify'); 10 | var git = require('gulp-git'); 11 | var bump = require('gulp-bump'); 12 | var argv = require('yargs').argv; 13 | 14 | function exec(cmd, options, cb) { 15 | 'use strict'; 16 | 17 | gutil.log('exec:', cmd); 18 | cb = cb || options; 19 | require('child_process').exec(cmd, options, function(err, stdout, stderr) { 20 | gutil.log(stdout, stderr); 21 | cb(err); 22 | }); 23 | } 24 | 25 | function build(cb) { 26 | 'use strict'; 27 | 28 | var mxmlcOptions = { 29 | 'use-network': true, 30 | 'static-link-runtime-shared-libraries': true, 31 | 'use-resource-bundle-metadata': true, 32 | 'accessible': false, 33 | 'allow-source-path-overlap': false, 34 | 'target-player': 11.1, 35 | 'locale': 'en_US', 36 | 'output': 'dist/Player.swf', 37 | 'debug': true, 38 | 'benchmark': false, 39 | 'verbose-stacktraces': false, 40 | 'strict': true, 41 | 'warnings': true, 42 | 'show-unused-type-selector-warnings': true, 43 | 'show-actionscript-warnings': true, 44 | 'show-binding-warnings': true, 45 | 'show-invalid-css-property-warnings': true, 46 | 'incremental': false, 47 | 'es': false, 48 | 'include-libraries': 'ext/as3corelib/bin/as3corelib.swc' 49 | }; 50 | 51 | var optString = _.reduce(mxmlcOptions, function(memo, value, index) { 52 | return memo + ' -' + index + '=' + value.toString(); 53 | }, ''); 54 | 55 | exec('./node_modules/.bin/mxmlc ' + optString + ' src/Player.as', cb); 56 | } 57 | 58 | gulp.task('lint-jshint', function() { 59 | 'use strict'; 60 | 61 | return gulp.src([ 'jslib/locomote.js', 'gulpfile.js' ]) 62 | .pipe(jshint()) 63 | .pipe(jshint.reporter('default')) 64 | .pipe(jshint.reporter('fail')); 65 | }); 66 | 67 | gulp.task('lint-jscs', function() { 68 | 'use strict'; 69 | 70 | return gulp.src([ 'jslib/locomote.js', 'gulpfile.js' ]) 71 | .pipe(jscs()); 72 | }); 73 | 74 | gulp.task('minify', function(cb) 75 | { 76 | 'use strict'; 77 | 78 | return gulp.src('jslib/locomote.js') 79 | .pipe(uglify()) 80 | .pipe(rename('locomote.min.js')) 81 | .pipe(gulp.dest('dist')); 82 | }); 83 | 84 | gulp.task('submodule', function(cb) { 85 | 'use strict'; 86 | 87 | return exec('git submodule init && git submodule update', cb); 88 | }); 89 | 90 | gulp.task('build-as3corelib', [ 'submodule' ], function(cb) { 91 | 'use strict'; 92 | 93 | if (fs.existsSync('ext/as3corelib/bin/as3corelib.swc')) { 94 | return cb(); 95 | } 96 | 97 | var options = { 98 | env: { 99 | FLEX_HOME: __dirname + '/node_modules/flex-sdk/lib/flex_sdk/' 100 | } 101 | }; 102 | exec('ant -f ext/as3corelib/build/build.xml', options, cb); 103 | }); 104 | 105 | gulp.task('build-locomote', [ 'build-as3corelib' ], function(cb) { 106 | 'use strict'; 107 | 108 | build(cb); 109 | }); 110 | 111 | gulp.task('build-locomote-version', [ 'build-as3corelib', 'version-file' ], function(cb) { 112 | 'use strict'; 113 | 114 | build(cb); 115 | }); 116 | 117 | gulp.task('package-version', function(cb) { 118 | 'use strict'; 119 | 120 | return gulp.src('package.json') 121 | .pipe(bump({ type:argv.ver })) 122 | .pipe(gulp.dest('')); 123 | }); 124 | 125 | gulp.task('bower-version', function(cb) { 126 | 'use strict'; 127 | 128 | return gulp.src('bower.json') 129 | .pipe(bump({ type:argv.ver })) 130 | .pipe(gulp.dest('')); 131 | }); 132 | 133 | gulp.task('version-file', [ 'package-version', 'bower-version' ], function() { 134 | 'use strict'; 135 | 136 | var pkg = require('./package.json'); 137 | var fs = require('fs'); 138 | fs.writeFile('VERSION', pkg.version); 139 | }); 140 | 141 | gulp.task('commit-release', [ 'build-locomote-version', 'minify' ], function(cb) { 142 | 'use strict'; 143 | 144 | var pkg = require('./package.json'); 145 | 146 | return gulp.src([ 'package.json', 'bower.json', 'VERSION', 'dist/locomote.min.js', 'dist/Player.swf' ]) 147 | .pipe(git.add({ args: '-f' })) 148 | .pipe(git.commit('Committed release, version ' + pkg.version + '.')); 149 | }); 150 | 151 | gulp.task('tag-release', [ 'commit-release' ], function() { 152 | 'use strict'; 153 | 154 | var pkg = require('./package.json'); 155 | 156 | git.tag('v' + pkg.version, 'Version message', function (err) { 157 | if (err) { 158 | throw err; 159 | } 160 | }); 161 | }); 162 | 163 | gulp.task('clean-release', [ 'tag-release' ], function() { 164 | 'use strict'; 165 | 166 | var pkg = require('./package.json'); 167 | 168 | return gulp.src([ 'dist/' ]) 169 | .pipe(rimraf()) 170 | .pipe(git.add({ args: '-f -A' })) 171 | .pipe(git.commit('Cleaned "dist/" folder after release ' + pkg.version + '.')); 172 | }); 173 | 174 | gulp.task('test', [ 'lint-jshint', 'lint-jscs' ]); 175 | 176 | gulp.task('default', [ 'build-as3corelib', 'build-locomote', 'minify' ]); 177 | 178 | gulp.task('release', [ 'clean-release' ]); 179 | 180 | gulp.task('clean', function() { 181 | 'use strict'; 182 | 183 | gulp.src([ 'dist/', 'ext/as3corelib/', 'VERSION' ], { read: false }) 184 | .pipe(rimraf({ force: true })); 185 | }); 186 | -------------------------------------------------------------------------------- /src/com/axis/mjpegclient/MJPEGClient.as: -------------------------------------------------------------------------------- 1 | package com.axis.mjpegclient { 2 | 3 | import com.axis.Logger; 4 | import com.axis.IClient; 5 | import com.axis.ClientEvent; 6 | import com.axis.mjpegclient.Image; 7 | import com.axis.mjpegclient.Handle; 8 | import com.axis.mjpegclient.MJPEG; 9 | import com.axis.ErrorManager; 10 | import flash.display.DisplayObject; 11 | import flash.display.Sprite; 12 | import flash.display.LoaderInfo; 13 | import flash.display.Stage; 14 | import flash.display.StageAlign; 15 | import flash.display.StageScaleMode; 16 | import flash.events.Event; 17 | import flash.events.EventDispatcher; 18 | 19 | public class MJPEGClient extends EventDispatcher implements IClient { 20 | private var handle:Handle; 21 | private var mjpeg:MJPEG; 22 | private var state:String = "initial"; 23 | private var streamBuffer:Array = new Array(); 24 | private var frameByFrame:Boolean = false; 25 | private var connectionBroken:Boolean = false; 26 | 27 | public function MJPEGClient(urlParsed:Object) { 28 | this.handle = new Handle(urlParsed); 29 | this.mjpeg = new MJPEG(Player.config.buffer * 1000); 30 | this.frameByFrame = Player.config.frameByFrame; 31 | 32 | mjpeg.addEventListener("frame", onFrame); 33 | mjpeg.addEventListener(MJPEG.BUFFER_EMPTY, onBufferEmpty); 34 | mjpeg.addEventListener(MJPEG.BUFFER_FULL, onBufferFull); 35 | mjpeg.addEventListener(MJPEG.IMAGE_ERROR, onMJPEGImageError); 36 | handle.addEventListener(Image.NEW_IMAGE_EVENT, onImage); 37 | handle.addEventListener(Handle.CONNECTED, onConnected); 38 | } 39 | 40 | public function getDisplayObject():DisplayObject { 41 | return this.mjpeg; 42 | }; 43 | 44 | public function getCurrentTime():Number { 45 | return this.mjpeg.getCurrentTime(); 46 | } 47 | 48 | public function bufferedTime():Number { 49 | return this.mjpeg.bufferedTime(); 50 | } 51 | 52 | public function start(options:Object):Boolean { 53 | this.handle.connect(); 54 | state = "connecting"; 55 | return true; 56 | } 57 | 58 | public function stop():Boolean { 59 | state = "stopped"; 60 | if (connectionBroken) { 61 | this.stopIfDone(); 62 | } else { 63 | this.handle.disconnect(); 64 | } 65 | 66 | return true; 67 | } 68 | 69 | public function seek(position:Number):Boolean { 70 | return false; 71 | } 72 | 73 | public function pause():Boolean { 74 | state = "paused"; 75 | this.mjpeg.pause(); 76 | dispatchEvent(new ClientEvent(ClientEvent.PAUSED, { 'reason': 'user' })); 77 | return true; 78 | } 79 | 80 | public function resume():Boolean { 81 | state = "playing"; 82 | this.mjpeg.resume(); 83 | dispatchEvent(new ClientEvent(ClientEvent.START_PLAY)); 84 | return true; 85 | } 86 | 87 | public function setFrameByFrame(frameByFrame:Boolean):Boolean { 88 | this.frameByFrame = frameByFrame; 89 | return true; 90 | } 91 | 92 | public function setBuffer(seconds:Number):Boolean { 93 | this.mjpeg.setBuffer(seconds * 1000); 94 | return true; 95 | } 96 | 97 | public function setKeepAlive(seconds:Number):Boolean { 98 | return false; 99 | } 100 | 101 | public function currentFPS():Number { 102 | return mjpeg.getFps(); 103 | } 104 | 105 | public function playFrames(timestamp:Number):void { 106 | while (this.streamBuffer.length > 0 && this.streamBuffer[0].timestamp <= timestamp) { 107 | mjpeg.addImage(this.streamBuffer.shift()); 108 | } 109 | this.stopIfDone(); 110 | } 111 | 112 | private function onImage(image:Image):void { 113 | if (this.frameByFrame) { 114 | this.streamBuffer.push(image); 115 | dispatchEvent(new ClientEvent(ClientEvent.FRAME, image.timestamp)); 116 | } else { 117 | mjpeg.addImage(image); 118 | this.stopIfDone(); 119 | } 120 | } 121 | 122 | private function stopIfDone():void { 123 | if (this.state === "stopped" || this.streamBuffer.length === 0 && this.connectionBroken && mjpeg.bufferedTime() === 0) { 124 | mjpeg.clear(); 125 | dispatchEvent(new ClientEvent(ClientEvent.STOPPED)); 126 | } 127 | } 128 | 129 | private function onConnected(e:Event):void { 130 | handle.addEventListener(Handle.CLOSED, onClosed); 131 | } 132 | 133 | private function onClosed(e:Event):void { 134 | if (this.state === "connecting") { 135 | this.state = "stopped"; 136 | ErrorManager.dispatchError(704); 137 | } 138 | this.connectionBroken = true; 139 | this.mjpeg.setBuffer(0); 140 | this.stopIfDone(); 141 | } 142 | 143 | private function onFrame(e:FrameEvent):void { 144 | var resolution:Object = { 145 | width: e.getFrame().width, 146 | height: e.getFrame().height 147 | }; 148 | Logger.log('MJPEG frame ready', resolution); 149 | 150 | state = "playing"; 151 | dispatchEvent(new ClientEvent(ClientEvent.META, resolution)); 152 | dispatchEvent(new ClientEvent(ClientEvent.START_PLAY)); 153 | mjpeg.removeEventListener("frame", onFrame); 154 | } 155 | 156 | private function onBufferEmpty(e:Event):void { 157 | Logger.log('MJPEG status buffer empty'); 158 | if (this.connectionBroken && this.streamBuffer.length === 0) { 159 | this.stopIfDone(); 160 | } else if (this.mjpeg.getBuffer() > 0) { 161 | dispatchEvent(new ClientEvent(ClientEvent.PAUSED, { 'reason': 'buffering' })); 162 | } 163 | } 164 | 165 | private function onBufferFull(e:Event):void { 166 | Logger.log('MJPEG status buffer full'); 167 | if (state === "playing") { 168 | dispatchEvent(new ClientEvent(ClientEvent.START_PLAY)); 169 | } 170 | } 171 | 172 | private function onMJPEGImageError(e:Event):void { 173 | ErrorManager.dispatchError(833); 174 | } 175 | 176 | } 177 | } 178 | -------------------------------------------------------------------------------- /src/com/axis/httpclient/HTTPClient.as: -------------------------------------------------------------------------------- 1 | package com.axis.httpclient { 2 | import com.axis.ClientEvent; 3 | import com.axis.NetStreamClient; 4 | import com.axis.ErrorManager; 5 | import com.axis.IClient; 6 | import com.axis.Logger; 7 | 8 | import flash.utils.*; 9 | import flash.events.Event; 10 | import flash.events.EventDispatcher; 11 | import flash.events.NetStatusEvent; 12 | import flash.media.Video; 13 | import flash.net.NetConnection; 14 | import flash.net.NetStream; 15 | 16 | public class HTTPClient extends NetStreamClient implements IClient { 17 | private var urlParsed:Object; 18 | private var nc:NetConnection; 19 | 20 | private static const FORCED_FPS:Number = 30; 21 | private static const UPDATE_VIRT_BUFFER_INTERVAL:Number = 1000 / FORCED_FPS; 22 | private var updateLoop:uint = 0; 23 | private var virtPause:Boolean = false; 24 | private var userPause:Boolean = false; 25 | private var frameByFrame:Boolean = false; 26 | private var virtBuffer:Number = 0; 27 | private var streamBuffer:Number = 0; 28 | 29 | public function HTTPClient(urlParsed:Object) { 30 | this.urlParsed = urlParsed; 31 | } 32 | 33 | public function start(options:Object):Boolean { 34 | Logger.log('HTTPClient: playing:', urlParsed.full); 35 | 36 | this.frameByFrame = Player.config.frameByFrame; 37 | nc = new NetConnection(); 38 | nc.connect(null); 39 | nc.addEventListener(NetStatusEvent.NET_STATUS, onConnectionStatus); 40 | 41 | this.ns = new NetStream(nc); 42 | this.setupNetStream(); 43 | 44 | this.ns.play(urlParsed.full); 45 | this.ns.addEventListener(NetStatusEvent.NET_STATUS, this.onNetStreamStatus); 46 | this.updateLoop = setInterval(this.updateVirtualBuffer, UPDATE_VIRT_BUFFER_INTERVAL); 47 | return true; 48 | } 49 | 50 | public function stop():Boolean { 51 | clearInterval(this.updateLoop); 52 | this.ns.dispose(); 53 | this.nc.close(); 54 | this.currentState = 'stopped'; 55 | dispatchEvent(new ClientEvent(ClientEvent.STOPPED)); 56 | return true; 57 | } 58 | 59 | public function seek(position:Number):Boolean { 60 | this.ns.seek(position); 61 | return true; 62 | } 63 | 64 | public function pause():Boolean { 65 | if (this.currentState !== 'playing') { 66 | ErrorManager.dispatchError(800); 67 | return false; 68 | } 69 | 70 | this.userPause = true; 71 | this.ns.pause(); 72 | 73 | if (this.virtPause) { 74 | dispatchEvent(new ClientEvent(ClientEvent.PAUSED, { 'reason': 'user' })); 75 | } 76 | 77 | return true; 78 | } 79 | 80 | public function resume():Boolean { 81 | if (this.currentState !== 'paused') { 82 | ErrorManager.dispatchError(801); 83 | return false; 84 | } 85 | this.userPause = false; 86 | if (!this.virtPause) { 87 | this.ns.resume(); 88 | } else { 89 | this.currentState = 'playing'; 90 | dispatchEvent(new ClientEvent(ClientEvent.START_PLAY)); 91 | } 92 | return true; 93 | } 94 | 95 | public function setFrameByFrame(frameByFrame:Boolean):Boolean { 96 | this.frameByFrame = frameByFrame; 97 | return true; 98 | } 99 | 100 | override public function bufferedTime():Number { 101 | var c:Number = this.getCurrentTime(); 102 | return c >= 0 ? Math.max(this.virtBuffer - c, 0) : c; 103 | } 104 | 105 | public function playFrames(timestamp:Number):void { 106 | if (this.virtBuffer < timestamp) { 107 | this.virtBuffer = timestamp; 108 | } 109 | } 110 | 111 | public function setBuffer(seconds:Number):Boolean { 112 | this.ns.bufferTime = seconds; 113 | if (!this.userPause && !this.virtPause) { 114 | this.ns.pause(); 115 | this.ns.resume(); 116 | } 117 | return true; 118 | } 119 | 120 | public function setKeepAlive(seconds:Number):Boolean { 121 | return false; 122 | } 123 | 124 | private function updateVirtualBuffer():void { 125 | if (this.currentState == 'stopped') { 126 | return; 127 | } 128 | 129 | if (this.frameByFrame) { 130 | var buffer:Number = this.getCurrentTime() + super.bufferedTime(); 131 | var step:Number = 1000 / FORCED_FPS; 132 | while (this.streamBuffer < buffer) { 133 | this.streamBuffer = Math.min(this.streamBuffer + step, buffer) 134 | dispatchEvent(new ClientEvent(ClientEvent.FRAME, this.streamBuffer)); 135 | } 136 | if (!this.virtPause && this.virtBuffer <= this.getCurrentTime() && !streamEnded) { 137 | Logger.log('HTTPClient: pause caused by virtual buffer ran out', { currentTime: this.getCurrentTime(), virtualBuffer: this.virtBuffer, realBuffer: super.bufferedTime() }); 138 | this.virtPause = true; 139 | // Prevent NetStreamClient from triggering paused event 140 | this.currentState = 'paused'; 141 | this.ns.pause(); 142 | } else if (this.virtPause && this.virtBuffer - this.getCurrentTime() >= this.ns.bufferTime * 1000) { 143 | this.virtPause = false; 144 | if (!this.userPause) { 145 | this.ns.resume(); 146 | } 147 | } 148 | } else { 149 | this.virtBuffer = this.getCurrentTime() + super.bufferedTime(); 150 | } 151 | } 152 | 153 | private function onNetStreamStatus(event:NetStatusEvent):void { 154 | if ('NetStream.Play.Stop' === event.info.code) { 155 | clearInterval(this.updateLoop); 156 | } 157 | } 158 | 159 | private function onConnectionStatus(event:NetStatusEvent):void { 160 | Logger.log('HTTPClient: connection status:', event.info.code); 161 | if ('NetConnection.Connect.Closed' === event.info.code && this.currentState !== 'stopped') { 162 | clearInterval(this.updateLoop); 163 | this.currentState = 'stopped'; 164 | dispatchEvent(new ClientEvent(ClientEvent.STOPPED)); 165 | this.ns.dispose(); 166 | } 167 | } 168 | } 169 | } 170 | -------------------------------------------------------------------------------- /src/com/axis/rtspclient/RTSPoverHTTPAPHandle.as: -------------------------------------------------------------------------------- 1 | package com.axis.rtspclient { 2 | import com.axis.ErrorManager; 3 | import com.axis.http.request; 4 | import com.axis.Logger; 5 | import com.axis.rtspclient.GUID; 6 | 7 | import flash.events.Event; 8 | import flash.events.HTTPStatusEvent; 9 | import flash.events.EventDispatcher; 10 | import flash.events.IOErrorEvent; 11 | import flash.events.ProgressEvent; 12 | import flash.events.SecurityErrorEvent; 13 | import flash.events.TimerEvent; 14 | 15 | import flash.net.URLStream; 16 | import flash.net.URLLoader; 17 | import flash.net.URLRequest; 18 | import flash.utils.ByteArray; 19 | import flash.utils.*; 20 | 21 | import mx.utils.Base64Encoder; 22 | 23 | public class RTSPoverHTTPAPHandle extends EventDispatcher implements IRTSPHandle { 24 | private var getChannel:URLStream = null; 25 | private var poster:URLLoader = null; 26 | private var urlParsed:Object; 27 | private var sessioncookie:String; 28 | private var url:String; 29 | private var connectTimer:Timer; 30 | 31 | private var base64encoder:Base64Encoder; 32 | 33 | private var secure:Boolean; 34 | 35 | public function RTSPoverHTTPAPHandle(urlParsed:Object, secure:Boolean) { 36 | this.sessioncookie = GUID.create(); 37 | this.urlParsed = urlParsed; 38 | this.url = 'https://' + this.urlParsed.host + this.urlParsed.urlpath + "?sessioncookie=" + this.sessioncookie; 39 | this.base64encoder = new Base64Encoder(); 40 | this.secure = secure; 41 | this.getChannel = new URLStream(); 42 | this.getChannel.addEventListener(ProgressEvent.PROGRESS, onData); 43 | this.getChannel.addEventListener(Event.OPEN, onOpen); 44 | this.getChannel.addEventListener(Event.COMPLETE, onComplete); 45 | this.getChannel.addEventListener(HTTPStatusEvent.HTTP_STATUS, onStatus); 46 | this.getChannel.addEventListener(IOErrorEvent.IO_ERROR, onIOError); 47 | this.getChannel.addEventListener(SecurityErrorEvent.SECURITY_ERROR, onSecurityError); 48 | } 49 | 50 | public function writeUTFBytes(value:String):void { 51 | var data:String = base64encode(value); 52 | var req:URLRequest = new URLRequest(this.url); 53 | req.method = 'POST'; 54 | req.data = data; 55 | req.contentType = 'application/x-rtsp-tunnelled'; 56 | 57 | this.poster = new URLLoader(); 58 | this.poster.addEventListener(IOErrorEvent.IO_ERROR, function ():void {}); 59 | this.poster.addEventListener(SecurityErrorEvent.SECURITY_ERROR, function ():void {}); 60 | this.poster.load(req); 61 | } 62 | 63 | public function readBytes(bytes:ByteArray, offset:uint = 0, length:uint = 0):void { 64 | this.getChannel.readBytes(bytes, offset, length); 65 | } 66 | 67 | public function disconnect():void { 68 | if (this.getChannel.connected) { 69 | this.getChannel.close(); 70 | this.getChannel.removeEventListener(ProgressEvent.PROGRESS, onData); 71 | this.getChannel.removeEventListener(Event.OPEN, onOpen); 72 | this.getChannel.removeEventListener(Event.COMPLETE, onComplete); 73 | this.getChannel.removeEventListener(HTTPStatusEvent.HTTP_STATUS, onStatus); 74 | this.getChannel.removeEventListener(IOErrorEvent.IO_ERROR, onIOError); 75 | this.getChannel.removeEventListener(SecurityErrorEvent.SECURITY_ERROR, onSecurityError); 76 | } 77 | 78 | /* should probably wait for close, but it doesn't seem to fire properly */ 79 | dispatchEvent(new Event('closed')); 80 | } 81 | 82 | public function connect():void { 83 | var req:URLRequest = new URLRequest(this.url); 84 | Logger.log('RTSP+HTTP+AxisProxy connecting to', req.url); 85 | this.getChannel.load(req); 86 | } 87 | 88 | public function reconnect():void { 89 | if (getChannel.connected) { 90 | getChannel.close(); 91 | } 92 | connect(); 93 | } 94 | 95 | public function cmdReceived():void { 96 | if (this.poster) { 97 | // Close the previous POST request 98 | this.poster.close(); 99 | this.poster.removeEventListener(IOErrorEvent.IO_ERROR, function ():void {}); 100 | this.poster.removeEventListener(SecurityErrorEvent.SECURITY_ERROR, function ():void {}); 101 | } 102 | } 103 | 104 | private function onComplete(event:Event):void { 105 | dispatchEvent(new Event('closed')); 106 | } 107 | 108 | private function onOpen(event:Event):void { 109 | Logger.log('RTSP+HTTP+AxisProxy connected to', 'http://' + this.urlParsed.host + 110 | this.urlParsed.urlpath + "?sessioncookie=" + sessioncookie); 111 | if(Player.isUserAgentIE()) { 112 | // IE/Edge workaround. getChannel OPEN event is emitted prematurely. 113 | // This causes the following POST request to be sent before the GET channel is established. 114 | // Add a delay to give some time for the GET connection to be established. 115 | this.connectTimer = new Timer(1000, 1); 116 | this.connectTimer.addEventListener(TimerEvent.TIMER, connectedDelay); 117 | this.connectTimer.start(); 118 | } else { 119 | dispatchEvent(new Event('connected')); 120 | } 121 | } 122 | 123 | private function connectedDelay(e:TimerEvent):void { 124 | dispatchEvent(new Event('connected')); 125 | } 126 | 127 | private function onData(event:ProgressEvent):void { 128 | dispatchEvent(new Event('data')); 129 | } 130 | 131 | private function onStatus(event:HTTPStatusEvent):void { 132 | if (event.status !== 200) { 133 | ErrorManager.dispatchError(event.status); 134 | } 135 | } 136 | 137 | private function onIOError(event:IOErrorEvent):void { 138 | ErrorManager.dispatchError(732, [event.text]); 139 | dispatchEvent(new Event('closed')); 140 | } 141 | 142 | private function onSecurityError(event:SecurityErrorEvent):void { 143 | ErrorManager.dispatchError(731, [event.text]); 144 | dispatchEvent(new Event('closed')); 145 | } 146 | 147 | private function base64encode(str:String):String { 148 | base64encoder.reset(); 149 | base64encoder.insertNewLines = false; 150 | base64encoder.encode(str); 151 | return base64encoder.toString(); 152 | } 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /src/com/axis/mjpegclient/MJPEG.as: -------------------------------------------------------------------------------- 1 | package com.axis.mjpegclient { 2 | 3 | import com.axis.mjpegclient.Image; 4 | 5 | import com.axis.Logger; 6 | import flash.display.Loader; 7 | import flash.display.Bitmap; 8 | import flash.display.LoaderInfo; 9 | import flash.display.Sprite; 10 | import flash.events.Event; 11 | import flash.events.IOErrorEvent; 12 | import flash.utils.*; 13 | 14 | [Event(name="frame",type="flash.events.Event")] 15 | 16 | /** 17 | * Display object that can be fed JPEG data and show it centered. 18 | */ 19 | public class MJPEG extends Sprite { 20 | 21 | public static const BUFFER_EMPTY:String = "bufferEmpty"; 22 | public static const BUFFER_FULL:String = "bufferFull"; 23 | public static const IMAGE_ERROR:String = "imageError"; 24 | 25 | private const FLOATING_AVG_LENGTH:Number = 10; 26 | 27 | private var bufferSize:Number; 28 | private var paused:Boolean = false; 29 | 30 | private var loadTimer:uint; 31 | private var busy:Boolean = false; 32 | private var buffering:Boolean = true; 33 | private var timestamps:Vector. = new Vector.(); 34 | private var loadTimes:Vector. = new Vector.(); 35 | private var imageBuffer:Vector. = new Vector.(); 36 | 37 | public function MJPEG(bufferSize:Number = 1000) { 38 | this.bufferSize = bufferSize; 39 | 40 | createLoaders(); 41 | 42 | /* needed for double-click (fullscreen) to work */ 43 | this.mouseChildren = false; 44 | this.doubleClickEnabled = true; 45 | } 46 | 47 | private function createLoaders():void { 48 | addChild(new Loader()); // Backbuffer 49 | addChild(new Loader()); // Frontbuffer 50 | } 51 | 52 | private function get backbuffer():Loader { 53 | return getChildAt(0) as Loader; 54 | } 55 | 56 | private function get frontBuffer():Loader { 57 | return getChildAt(1) as Loader; 58 | } 59 | 60 | private function get firstTimestamp():Number { 61 | return this.timestamps.length > 0 ? this.timestamps[0] : 0; 62 | } 63 | 64 | private function get lastTimestamp():Number { 65 | return timestamps.length > 0 ? timestamps[timestamps.length - 1] : 0; 66 | } 67 | 68 | private function get firstLoadTime():Number { 69 | return this.loadTimes.length > 0 ? this.loadTimes[0] : -1; 70 | } 71 | 72 | private function get lastLoadTime():Number { 73 | return loadTimes.length > 0 ? loadTimes[loadTimes.length - 1] : -1; 74 | } 75 | 76 | private function destroyLoaders():void { 77 | removeLoaderEventListeners(backbuffer); 78 | removeChildren(); 79 | } 80 | 81 | private function timeUntilLoad(image:Image):Number { 82 | if (this.timestamps.length === 0 || this.firstLoadTime === -1) { 83 | return 0; 84 | } 85 | var diff:Number = image.timestamp - this.lastTimestamp; 86 | var time:Number = diff - (new Date().getTime() - this.lastLoadTime); 87 | return time <= 0 ? 0 : time; 88 | } 89 | 90 | public function pause():void { 91 | this.paused = true; 92 | this.loadNext(); 93 | } 94 | 95 | public function resume():void { 96 | this.paused = false; 97 | this.loadNext(); 98 | } 99 | 100 | public function getFps():Number { 101 | if (timestamps.length < 2) { return 0; } 102 | var loadTimesSum:Number = 0; 103 | var idx:int = timestamps.length - FLOATING_AVG_LENGTH; 104 | for (var i:uint = idx > 0 ? idx : 1; i < timestamps.length; i++) { 105 | loadTimesSum += timestamps[i] - timestamps[i - 1]; 106 | } 107 | return 1000 * (timestamps.length - 1) / loadTimesSum; 108 | } 109 | 110 | public function getCurrentTime():Number { 111 | return timestamps.length > 0 ? this.lastTimestamp - this.firstTimestamp : 0; 112 | } 113 | 114 | public function setBuffer(bufferSize:Number):void { 115 | this.bufferSize = bufferSize; 116 | this.addImage(); 117 | } 118 | 119 | public function getBuffer():Number { 120 | return this.bufferSize; 121 | } 122 | 123 | public function bufferedTime():Number { 124 | if (this.imageBuffer.length === 0) { 125 | return 0; 126 | } 127 | return this.imageBuffer[this.imageBuffer.length - 1].timestamp - this.imageBuffer[0].timestamp + this.timeUntilLoad(this.imageBuffer[0]); 128 | } 129 | 130 | public function addImage(image:Image = null):void { 131 | /* This function can be used to trigger reevaluation of the buffer state 132 | * if run without arguments */ 133 | image !== null && this.imageBuffer.push(image); 134 | 135 | if (this.buffering && this.bufferedTime() > this.bufferSize) { 136 | dispatchEvent(new Event(MJPEG.BUFFER_FULL)); 137 | this.buffering = false; 138 | } 139 | 140 | if (!this.buffering) { 141 | this.loadNext(); 142 | } 143 | } 144 | 145 | private function loadNext():void { 146 | if (busy || this.imageBuffer.length === 0 || this.paused) { 147 | /* Already in the process of decoding an image, ignore this new image data */ 148 | return; 149 | } 150 | busy = true; 151 | 152 | var image:Image = this.imageBuffer.shift() 153 | 154 | 155 | var timeout:Number = this.timeUntilLoad(image); 156 | 157 | this.loadTimes.push(new Date().getTime() + timeout); 158 | this.loadTimer = setTimeout(this.doLoad, timeout, image); 159 | } 160 | 161 | private function doLoad(image:Image):void { 162 | this.timestamps.push(image.timestamp); 163 | addLoaderEventListeners(backbuffer); 164 | backbuffer.loadBytes(image.data) 165 | } 166 | 167 | private function onLoadComplete(event:Event):void { 168 | var bitmap:Bitmap = event.currentTarget.content; 169 | if (bitmap != null) { 170 | bitmap.smoothing = true; 171 | } 172 | 173 | // Will crash if not removing listeners before swaping children 174 | removeLoaderEventListeners(backbuffer); 175 | this.swapChildren(frontBuffer, backbuffer); 176 | 177 | busy = false; 178 | 179 | if (this.imageBuffer.length === 0) { 180 | this.buffering = true; 181 | dispatchEvent(new Event(MJPEG.BUFFER_EMPTY)); 182 | } else { 183 | this.loadNext(); 184 | } 185 | 186 | dispatchEvent(new FrameEvent(bitmap)); 187 | } 188 | 189 | private function onImageError(event:IOErrorEvent):void { 190 | busy = false; 191 | var loader:Loader = event.currentTarget.loader; 192 | removeLoaderEventListeners(loader); 193 | Logger.log('MJPEG failed to load image.', event.toString()); 194 | dispatchEvent(new Event(MJPEG.IMAGE_ERROR)); 195 | } 196 | 197 | private function addLoaderEventListeners(loader:Loader):void { 198 | loader.contentLoaderInfo.addEventListener(Event.COMPLETE, onLoadComplete); 199 | loader.contentLoaderInfo.addEventListener(IOErrorEvent.IO_ERROR, onImageError); 200 | } 201 | 202 | private function removeLoaderEventListeners(loader:Loader):void { 203 | loader.contentLoaderInfo.removeEventListener(Event.COMPLETE, onLoadComplete); 204 | loader.contentLoaderInfo.removeEventListener(IOErrorEvent.IO_ERROR, onImageError); 205 | } 206 | 207 | public function clear():void { 208 | clearTimeout(this.loadTimer); 209 | // Remove graphics components, abort play 210 | destroyLoaders(); 211 | busy = false; 212 | } 213 | } 214 | } 215 | -------------------------------------------------------------------------------- /jslib/locomote.js: -------------------------------------------------------------------------------- 1 | (function(root, factory) { 2 | 'use strict'; 3 | 4 | if (typeof exports == 'object') { 5 | /* CommonJS */ 6 | module.exports = factory(); 7 | } else if (typeof define == 'function' && define.amd) { 8 | /* AMD module */ 9 | define(factory); 10 | } else { 11 | /* Browser global */ 12 | root.Locomote = factory(); 13 | } 14 | } 15 | (this, function() { 16 | 'use strict'; 17 | 18 | function Locomote(tag, swf) { 19 | if (!tag) { 20 | return null; 21 | } 22 | 23 | if (!window.LocomoteMap) { 24 | window.LocomoteMap = {}; 25 | } 26 | 27 | // Instance already initialized. Return it. 28 | if (window.LocomoteMap[tag] && 29 | window.LocomoteMap[tag].swfready) { 30 | return window.LocomoteMap[tag]; 31 | } 32 | 33 | // return a new Locomote object if we're in the global scope 34 | if (this === undefined) { 35 | window.LocomoteMap[tag] = new Locomote(tag, swf); 36 | return window.LocomoteMap[tag]; 37 | } 38 | 39 | // Init our element object and return the object 40 | window.LocomoteMap[tag] = this; 41 | this.callbacks = []; 42 | this.swfready = false; 43 | this.__embed(tag, swf); 44 | return this; 45 | } 46 | 47 | Locomote.prototype = { 48 | __embed: function(tag, swf) { 49 | this.tag = tag; 50 | 51 | var guid = (function() { 52 | function s4() { 53 | return Math.floor((1 + Math.random()) * 0x10000) 54 | .toString(16) 55 | .substring(1); 56 | } 57 | return function() { 58 | return s4() + s4() + '-' + s4() + '-' + s4() + '-' + s4() + '-' + s4() + s4() + s4(); 59 | }; 60 | })(); 61 | var tempTag = guid(); 62 | var element = 63 | ''; 71 | 72 | // Default Flash Player options 73 | var opts = { 74 | width: '100%', 75 | height: '100%', 76 | allowscriptaccess: 'always', 77 | wmode: 'transparent', 78 | quality: 'high', 79 | flashvars: 'locomoteID=' + tag, 80 | allowFullScreenInteractive: true, 81 | movie: swf, 82 | name: tag 83 | }; 84 | 85 | for (var index in opts) { 86 | if (opts.hasOwnProperty(index)) { 87 | element += ''; 88 | } 89 | } 90 | 91 | element += ''; 92 | 93 | if (('string' === typeof tag) && document.getElementById(tag)) { 94 | // Insert the object into the provided tag 95 | document.getElementById(tag).innerHTML = element; 96 | 97 | // Save the reference to the Flash Player object 98 | this.e = document.getElementById(tempTag); 99 | } else { 100 | // Insert the object into the provided element 101 | tag.innerHTML = element; 102 | 103 | // Save the reference to the Flash Player object 104 | var players = tag.getElementsByClassName('locomote-player'); 105 | 106 | for (var i = 0; i < players.length; i++) { 107 | if (players[i].getAttribute('id') === tempTag) { 108 | this.e = players[i]; 109 | break; 110 | } 111 | } 112 | } 113 | }, 114 | 115 | __swfReady: function() { 116 | this.swfready = true; 117 | this.__playerEvent('apiReady'); 118 | }, 119 | 120 | play: function(url, options) { 121 | options = options || {}; 122 | this.e.play(url, options); 123 | return this; 124 | }, 125 | 126 | stop: function() { 127 | this.e.stop(); 128 | return this; 129 | }, 130 | 131 | seek: function(position) { 132 | this.e.seek(position); 133 | return this; 134 | }, 135 | 136 | pause: function() { 137 | this.e.pause(); 138 | return this; 139 | }, 140 | 141 | resume: function() { 142 | this.e.resume(); 143 | return this; 144 | }, 145 | 146 | playFrames: function(timestamp) { 147 | this.e.playFrames(timestamp); 148 | return this; 149 | }, 150 | 151 | streamStatus: function() { 152 | return this.e.streamStatus(); 153 | }, 154 | 155 | playerStatus: function() { 156 | return this.e.playerStatus(); 157 | }, 158 | 159 | speakerVolume: function(volume) { 160 | this.e.speakerVolume(volume); 161 | return this; 162 | }, 163 | 164 | muteSpeaker: function() { 165 | this.e.muteSpeaker(); 166 | return this; 167 | }, 168 | 169 | unmuteSpeaker: function() { 170 | this.e.unmuteSpeaker(); 171 | return this; 172 | }, 173 | 174 | microphoneVolume: function(volume) { 175 | this.e.microphoneVolume(volume); 176 | return this; 177 | }, 178 | 179 | muteMicrophone: function() { 180 | this.e.muteMicrophone(); 181 | return this; 182 | }, 183 | 184 | unmuteMicrophone: function() { 185 | this.e.unmuteMicrophone(); 186 | return this; 187 | }, 188 | 189 | startAudioTransmit: function(url, type) { 190 | this.e.startAudioTransmit(url, type || 'axis'); 191 | return this; 192 | }, 193 | 194 | stopAudioTransmit: function() { 195 | this.e.stopAudioTransmit(); 196 | return this; 197 | }, 198 | 199 | config: function(config) { 200 | this.e.setConfig(config); 201 | return this; 202 | }, 203 | 204 | loadPolicyFile: function(url) { 205 | this.e.loadPolicyFile(url); 206 | return this; 207 | }, 208 | 209 | on: function(eventName, callback) { 210 | this.callbacks.push({ eventName: eventName, callback: callback }); 211 | 212 | if (eventName === 'apiReady' && this.swfready) { 213 | callback.call(); 214 | } 215 | return this; 216 | }, 217 | 218 | off: function(eventName, callback) { 219 | if (!eventName && !callback) { 220 | this.callbacks = []; 221 | return this; 222 | } 223 | 224 | this.callbacks.forEach(function(element, index, array) { 225 | if (element.callback === callback) { 226 | if (!eventName || (element.eventName === eventName)) { 227 | array.splice(index, 1); 228 | return this; 229 | } 230 | } 231 | 232 | if (element.eventName === eventName) { 233 | if (!callback || (element.callback === callback)) { 234 | array.splice(index, 1); 235 | } 236 | } 237 | }); 238 | return this; 239 | }, 240 | 241 | __playerEvent: function(eventName /* ... args */) { 242 | var params = []; 243 | params.push.apply(params, arguments); 244 | params.shift(); /* First element is event name */ 245 | this.callbacks.forEach(function(element, index, array) { 246 | if (element.eventName === eventName && element.callback) { 247 | element.callback.apply(null, params); 248 | } 249 | }); 250 | }, 251 | 252 | destroy: function() { 253 | window.LocomoteMap[this.tag] = { 254 | __playerEvent: function () {}, 255 | __swfReady: function () {} 256 | }; 257 | typeof this.e.stop === 'function' && this.e.stop(); 258 | this.e.parentNode.removeChild(this.e); 259 | this.e = null; 260 | } 261 | }; 262 | 263 | return Locomote; 264 | })); 265 | -------------------------------------------------------------------------------- /src/com/axis/rtspclient/RTSPoverHTTPHandle.as: -------------------------------------------------------------------------------- 1 | package com.axis.rtspclient { 2 | import com.axis.ClientEvent; 3 | import com.axis.ErrorManager; 4 | import com.axis.http.auth; 5 | import com.axis.http.request; 6 | import com.axis.http.url; 7 | import com.axis.Logger; 8 | import com.axis.rtspclient.GUID; 9 | 10 | import flash.events.ErrorEvent; 11 | import flash.events.Event; 12 | import flash.events.EventDispatcher; 13 | import flash.events.IOErrorEvent; 14 | import flash.events.ProgressEvent; 15 | import flash.events.SecurityErrorEvent; 16 | import flash.net.Socket; 17 | import flash.net.SecureSocket; 18 | import flash.utils.ByteArray; 19 | import flash.utils.*; 20 | 21 | import mx.utils.Base64Encoder; 22 | 23 | public class RTSPoverHTTPHandle extends EventDispatcher implements IRTSPHandle { 24 | private var getChannel:Socket = null; 25 | private var urlParsed:Object = {}; 26 | private var sessioncookie:String = ""; 27 | 28 | private var base64encoder:Base64Encoder; 29 | 30 | private var secure:Boolean; 31 | 32 | private var datacb:Function = null; 33 | private var connectcb:Function = null; 34 | 35 | private var authState:String = "none"; 36 | private var authOpts:Object = {}; 37 | private var digestNC:uint = 1; 38 | 39 | private var getChannelData:ByteArray; 40 | 41 | public function RTSPoverHTTPHandle(urlParsed:Object, secure:Boolean) { 42 | this.sessioncookie = GUID.create(); 43 | this.urlParsed = urlParsed; 44 | this.base64encoder = new Base64Encoder(); 45 | this.secure = secure; 46 | } 47 | 48 | private function setupSockets():void { 49 | getChannel = this.secure ? new SecureSocket() : new Socket(); 50 | getChannel.timeout = 5000; 51 | getChannel.addEventListener(Event.CONNECT, onGetChannelConnect); 52 | getChannel.addEventListener(ProgressEvent.SOCKET_DATA, onGetChannelData); 53 | getChannel.addEventListener(IOErrorEvent.IO_ERROR, onIOError); 54 | getChannel.addEventListener(SecurityErrorEvent.SECURITY_ERROR, onSecurityError); 55 | 56 | getChannelData = new ByteArray(); 57 | } 58 | 59 | private function base64encode(str:String):String { 60 | base64encoder.reset(); 61 | base64encoder.insertNewLines = false; 62 | base64encoder.encode(str); 63 | return base64encoder.toString(); 64 | } 65 | 66 | public function writeUTFBytes(value:String):void { 67 | var data:String = base64encode(value); 68 | var authHeader:String = auth.authorizationHeader("POST", authState, authOpts, urlParsed, digestNC++); 69 | var socket:Socket = this.secure ? new SecureSocket() : new Socket(); 70 | socket.timeout = 5000; 71 | socket.addEventListener(IOErrorEvent.IO_ERROR, onIOError); 72 | socket.addEventListener(SecurityErrorEvent.SECURITY_ERROR, onSecurityError); 73 | 74 | socket.addEventListener(Event.CONNECT, function ():void { 75 | socket.writeUTFBytes("POST " + urlParsed.urlpath + " HTTP/1.0\r\n"); 76 | socket.writeUTFBytes("X-Sessioncookie: " + sessioncookie + "\r\n"); 77 | socket.writeUTFBytes("Content-Length: " + data.length + "\r\n"); 78 | socket.writeUTFBytes("Content-Type: application/x-rtsp-tunnelled" + "\r\n"); 79 | socket.writeUTFBytes(authHeader); 80 | socket.writeUTFBytes("\r\n"); 81 | 82 | socket.writeUTFBytes(data); 83 | socket.flush(); 84 | 85 | // Timeout required before close to let the data actually be written to 86 | // the socket. Flush appears to be asynchronous... 87 | setTimeout(socket.close, 5000); 88 | }); 89 | 90 | 91 | socket.connect(this.urlParsed.host, this.urlParsed.port); 92 | } 93 | 94 | public function readBytes(bytes:ByteArray, offset:uint = 0, length:uint = 0):void { 95 | getChannel.readBytes(bytes, offset, length); 96 | } 97 | 98 | public function disconnect():void { 99 | if (getChannel.connected) { 100 | getChannel.close(); 101 | 102 | getChannel.removeEventListener(Event.CONNECT, onGetChannelConnect); 103 | getChannel.removeEventListener(ProgressEvent.SOCKET_DATA, onGetChannelData); 104 | getChannel.removeEventListener(IOErrorEvent.IO_ERROR, onIOError); 105 | getChannel.removeEventListener(SecurityErrorEvent.SECURITY_ERROR, onSecurityError); 106 | } 107 | 108 | /* should probably wait for close, but it doesn't seem to fire properly */ 109 | dispatchEvent(new Event('closed')); 110 | } 111 | 112 | public function connect():void { 113 | setupSockets(); 114 | Logger.log('RTSP+HTTP' + (this.secure ? 'S' : '') + 'connecting to', this.urlParsed.host + ':' + this.urlParsed.port); 115 | getChannel.connect(this.urlParsed.host, this.urlParsed.port); 116 | } 117 | 118 | public function reconnect():void { 119 | if (getChannel.connected) { 120 | getChannel.close(); 121 | } 122 | connect(); 123 | } 124 | 125 | // Not applicable 126 | public function cmdReceived():void {} 127 | 128 | private function onGetChannelConnect(event:Event):void { 129 | initializeGetChannel(); 130 | } 131 | 132 | public function stop():void { 133 | disconnect(); 134 | } 135 | 136 | private function onGetChannelData(event:ProgressEvent):void { 137 | var parsed:* = request.readHeaders(getChannel, getChannelData); 138 | if (false === parsed) { 139 | return; 140 | } 141 | 142 | if (401 === parsed.code) { 143 | Logger.log('Unauthorized using auth method: ' + authState); 144 | /* Unauthorized, change authState and (possibly) try again */ 145 | authOpts = parsed.headers['www-authenticate']; 146 | var newAuthState:String = auth.nextMethod(authState, authOpts); 147 | if (authState === newAuthState) { 148 | ErrorManager.dispatchError(parsed.code); 149 | return; 150 | } 151 | 152 | Logger.log('switching http-authorization from ' + authState + ' to ' + newAuthState); 153 | authState = newAuthState; 154 | getChannelData = new ByteArray(); 155 | getChannel.close(); 156 | getChannel.connect(this.urlParsed.host, this.urlParsed.port); 157 | return; 158 | } 159 | 160 | if (200 !== parsed.code) { 161 | ErrorManager.dispatchError(parsed.code); 162 | return; 163 | } 164 | 165 | getChannel.removeEventListener(ProgressEvent.SOCKET_DATA, onGetChannelData); 166 | getChannel.addEventListener(ProgressEvent.SOCKET_DATA, function(ev:ProgressEvent):void { 167 | dispatchEvent(new Event('data')); 168 | }); 169 | 170 | dispatchEvent(new Event('connected')); 171 | } 172 | 173 | private function initializeGetChannel():void { 174 | getChannel.writeUTFBytes("GET " + urlParsed.urlpath + " HTTP/1.0\r\n"); 175 | getChannel.writeUTFBytes("X-Sessioncookie: " + sessioncookie + "\r\n"); 176 | getChannel.writeUTFBytes("Accept: application/x-rtsp-tunnelled\r\n"); 177 | getChannel.writeUTFBytes(auth.authorizationHeader("GET", authState, authOpts, urlParsed, digestNC++)); 178 | getChannel.writeUTFBytes("\r\n"); 179 | getChannel.flush(); 180 | } 181 | 182 | private function onIOError(event:IOErrorEvent):void { 183 | ErrorManager.dispatchError(732, [event.text]); 184 | dispatchEvent(new Event('closed')); 185 | } 186 | 187 | private function onSecurityError(event:SecurityErrorEvent):void { 188 | ErrorManager.dispatchError(731, [event.text]); 189 | dispatchEvent(new Event('closed')); 190 | } 191 | } 192 | } 193 | -------------------------------------------------------------------------------- /src/com/axis/NetStreamClient.as: -------------------------------------------------------------------------------- 1 | package com.axis { 2 | import flash.display.DisplayObject; 3 | import flash.events.EventDispatcher; 4 | import flash.events.AsyncErrorEvent; 5 | import flash.events.DRMErrorEvent; 6 | import flash.events.IOErrorEvent; 7 | import flash.events.NetStatusEvent; 8 | import flash.media.Video; 9 | import flash.net.NetStream; 10 | 11 | import mx.utils.ObjectUtil; 12 | 13 | public class NetStreamClient extends EventDispatcher { 14 | public var onXMPData:Function = null; 15 | public var onCuePoint:Function = null; 16 | public var onImageData:Function = null; 17 | public var onSeekPoint:Function = null; 18 | public var onTextData:Function = null; 19 | 20 | private var video:Video = new Video(); 21 | protected var ns:NetStream; 22 | protected var currentState:String = 'stopped'; 23 | protected var streamEnded:Boolean = false; 24 | protected var bufferEmpty:Boolean = true; 25 | 26 | public function hasStreamEnded():Boolean { 27 | return this.streamEnded; 28 | } 29 | 30 | public function currentFPS():Number { 31 | return Math.floor(this.ns.currentFPS + 0.5); 32 | } 33 | 34 | public function getCurrentTime():Number { 35 | return (this.ns) ? this.ns.time * 1000 : -1; 36 | } 37 | 38 | public function bufferedTime():Number { 39 | return (this.ns) ? this.ns.bufferLength * 1000 : -1; 40 | } 41 | 42 | protected function setupNetStream():void { 43 | this.ns.bufferTime = Player.config.buffer; 44 | this.ns.client = this; 45 | this.onXMPData = onXMPDataHandler; 46 | this.onCuePoint = onCuePointHandler; 47 | this.onImageData = onImageDataHandler; 48 | this.onSeekPoint = onSeekPointHandler; 49 | this.onTextData = onTextDataHandler; 50 | this.ns.addEventListener(NetStatusEvent.NET_STATUS, onNetStatus); 51 | this.ns.addEventListener(AsyncErrorEvent.ASYNC_ERROR, onAsyncError); 52 | this.ns.addEventListener(DRMErrorEvent.DRM_ERROR, onDRMError); 53 | this.ns.addEventListener(IOErrorEvent.IO_ERROR, onIOError); 54 | this.video.attachNetStream(this.ns); 55 | } 56 | 57 | public function getDisplayObject():DisplayObject { 58 | return this.video; 59 | } 60 | 61 | public function onXMPDataHandler(xmpData:Object):void { 62 | Logger.log('XMPData received->' + xmpData.data); 63 | } 64 | 65 | public function onCuePointHandler(cuePoint:Object):void { 66 | Logger.log('CuePoint received: ' + cuePoint.name); 67 | } 68 | 69 | public function onImageDataHandler(imageData:Object):void { 70 | Logger.log('ImageData received'); 71 | } 72 | 73 | public function onSeekPointHandler(seekPoint:Object):void { 74 | Logger.log('SeekPoint received'); 75 | } 76 | 77 | public function onTextDataHandler(textData:Object):void { 78 | Logger.log('TextData received'); 79 | } 80 | 81 | private function onAsyncError(event:AsyncErrorEvent):void { 82 | ErrorManager.dispatchError(725, [event.error.message]); 83 | } 84 | 85 | private function onDRMError(event:DRMErrorEvent):void { 86 | ErrorManager.dispatchError(726, [event.errorID, event.subErrorID]); 87 | } 88 | 89 | private function onIOError(event:IOErrorEvent):void { 90 | ErrorManager.dispatchError(727, [event.text]); 91 | } 92 | 93 | public function onMetaData(item:Object):void { 94 | Logger.log('Netstream Metadata:', ObjectUtil.toString(item)); 95 | 96 | dispatchEvent(new ClientEvent(ClientEvent.META, { 97 | 'width': item.width, 98 | 'height': item.height, 99 | 'duration': item.duration 100 | })); 101 | } 102 | 103 | public function onPlayStatus(event:Object):void { 104 | Logger.log('onPlayStatus:', event.code); 105 | if (event.code === 'NetStream.Play.Complete') { 106 | streamEnded = true; 107 | } 108 | } 109 | 110 | private function onNetStatus(event:NetStatusEvent):void { 111 | Logger.log('NetStream status:', { 112 | event: event.info.code, 113 | ended: streamEnded, 114 | bufferEmpty: bufferEmpty, 115 | buffer: this.bufferedTime(), 116 | currentTime: this.getCurrentTime() 117 | }); 118 | 119 | if (!streamEnded && !bufferEmpty && ('NetStream.Play.Start' === event.info.code || 'NetStream.Unpause.Notify' === event.info.code)) { 120 | this.currentState = 'playing'; 121 | dispatchEvent(new ClientEvent(ClientEvent.START_PLAY)); 122 | return; 123 | } 124 | 125 | if ('NetStream.Play.Stop' === event.info.code) { 126 | dispatchEvent(new ClientEvent(ClientEvent.STOPPED)); 127 | this.ns.dispose(); 128 | return; 129 | } 130 | 131 | if ('NetStream.Buffer.Flush' === event.info.code) { 132 | streamEnded = true; 133 | } 134 | 135 | if ('NetStream.Buffer.Empty' === event.info.code) { 136 | bufferEmpty = true; 137 | if (streamEnded) { 138 | dispatchEvent(new ClientEvent(ClientEvent.STOPPED)); 139 | this.ns.dispose(); 140 | } else { 141 | this.currentState = 'buffering'; 142 | dispatchEvent(new ClientEvent(ClientEvent.PAUSED, { 'reason': 'buffering' })); 143 | } 144 | return; 145 | } 146 | 147 | if (!streamEnded && 'NetStream.Buffer.Full' === event.info.code) { 148 | bufferEmpty = false; 149 | this.currentState = 'playing'; 150 | dispatchEvent(new ClientEvent(ClientEvent.START_PLAY)); 151 | return; 152 | } 153 | 154 | if (this.currentState != 'paused' && 'NetStream.Pause.Notify' === event.info.code) { 155 | this.currentState = 'paused'; 156 | dispatchEvent(new ClientEvent(ClientEvent.PAUSED, { 'reason': 'user' })); 157 | return; 158 | } 159 | 160 | if (event.info.status === 'error' || event.info.level === 'error') { 161 | var errorCode:int = 0; 162 | switch (event.info.code) { 163 | case 'NetConnection.Call.BadVersion': errorCode = 700; break; 164 | case 'NetConnection.Call.Failed': errorCode = 701; break; 165 | case 'NetConnection.Call.Prohibited': errorCode = 702; break; 166 | case 'NetConnection.Connect.AppShutdown': errorCode = 703; break; 167 | case 'NetConnection.Connect.Failed': errorCode = 704; break; 168 | case 'NetConnection.Connect.InvalidApp': errorCode = 705; break; 169 | case 'NetConnection.Connect.Rejected': errorCode = 706; break; 170 | case 'NetGroup.Connect.Failed': errorCode = 707; break; 171 | case 'NetGroup.Connect.Rejected': errorCode = 708; break; 172 | case 'NetStream.Connect.Failed': errorCode = 709; break; 173 | case 'NetStream.Connect.Rejected': errorCode = 710; break; 174 | case 'NetStream.Failed': errorCode = 711; break; 175 | case 'NetStream.Play.Failed': errorCode = 712; break; 176 | case 'NetStream.Play.FileStructureInvalid': errorCode = 713; break; 177 | case 'NetStream.Play.InsufficientBW': errorCode = 714; break; 178 | case 'NetStream.Play.StreamNotFound': errorCode = 715; break; 179 | case 'NetStream.Publish.BadName': errorCode = 716; break; 180 | case 'NetStream.Record.Failed': errorCode = 717; break; 181 | case 'NetStream.Record.NoAccess': errorCode = 718; break; 182 | case 'NetStream.Seek.Failed': errorCode = 719; break; 183 | case 'NetStream.Seek.InvalidTime': errorCode = 720; break; 184 | case 'SharedObject.BadPersistence': errorCode = 721; break; 185 | case 'SharedObject.Flush.Failed': errorCode = 722; break; 186 | case 'SharedObject.UriMismatch': errorCode = 723; break; 187 | 188 | default: 189 | ErrorManager.dispatchError(724, [event.info.code]); 190 | return; 191 | } 192 | 193 | if (errorCode) { 194 | ErrorManager.dispatchError(errorCode); 195 | } 196 | } 197 | } 198 | } 199 | } 200 | -------------------------------------------------------------------------------- /src/com/axis/rtspclient/SDP.as: -------------------------------------------------------------------------------- 1 | package com.axis.rtspclient { 2 | import com.axis.ErrorManager; 3 | import com.axis.Logger; 4 | 5 | import flash.utils.ByteArray; 6 | 7 | import mx.utils.StringUtil; 8 | 9 | public class SDP { 10 | private var version:int = -1; 11 | private var origin:Object; 12 | private var sessionName:String; 13 | private var timing:Object; 14 | private var sessionBlock:Object = new Object(); 15 | private var media:Object = new Object(); 16 | 17 | public function SDP() { 18 | } 19 | 20 | public function parse(content:ByteArray):Boolean { 21 | var dataString:String = content.toString(); 22 | var success:Boolean = true; 23 | var currentMediaBlock:Object = sessionBlock; 24 | 25 | Logger.log(dataString); 26 | 27 | for each (var line:String in dataString.split("\n")) { 28 | line = line.replace(/\r/, ''); /* Delimiter '\r\n' is allowed, if this is the case, remove '\r' too */ 29 | if (0 === line.length) { 30 | /* Empty row (last row perhaps?), skip to next */ 31 | continue; 32 | } 33 | 34 | switch (line.charAt(0)) { 35 | case 'v': 36 | if (-1 !== version) { 37 | Logger.log('Version present multiple times in SDP'); 38 | return false; 39 | } 40 | success &&= parseVersion(line); 41 | break; 42 | 43 | case 'o': 44 | if (null !== origin) { 45 | Logger.log('Origin present multiple times in SDP'); 46 | return false; 47 | } 48 | success &&= parseOrigin(line); 49 | break; 50 | 51 | case 's': 52 | if (null !== sessionName) { 53 | Logger.log('Session Name present multiple times in SDP'); 54 | return false; 55 | } 56 | success &&= parseSessionName(line); 57 | break; 58 | 59 | case 't': 60 | if (null !== timing) { 61 | Logger.log('Timing present multiple times in SDP'); 62 | return false; 63 | } 64 | success &&= parseTiming(line); 65 | break; 66 | 67 | case 'm': 68 | if (null !== currentMediaBlock && sessionBlock !== currentMediaBlock) { 69 | /* Complete previous block and store it */ 70 | media[currentMediaBlock.type] = currentMediaBlock; 71 | } 72 | 73 | /* A wild media block appears */ 74 | currentMediaBlock = new Object(); 75 | currentMediaBlock.rtpmap = new Object(); 76 | parseMediaDescription(line, currentMediaBlock); 77 | break; 78 | 79 | case 'a': 80 | parseAttribute(line, currentMediaBlock); 81 | break; 82 | 83 | default: 84 | Logger.log('Ignored unknown SDP directive: ' + line); 85 | break; 86 | } 87 | } 88 | 89 | media[currentMediaBlock.type] = currentMediaBlock; 90 | 91 | return success; 92 | } 93 | 94 | private function parseVersion(line:String):Boolean { 95 | var matches:Array = line.match(/^v=([0-9]+)$/); 96 | if (0 === matches.length) { 97 | Logger.log('\'v=\' (Version) formatted incorrectly: ' + line); 98 | return false; 99 | } 100 | 101 | version = matches[1]; 102 | if (0 !== version) { 103 | Logger.log('Unsupported SDP version:' + version); 104 | return false; 105 | } 106 | 107 | return true; 108 | } 109 | 110 | private function parseOrigin(line:String):Boolean { 111 | var matches:Array = line.match(/^o=([^ ]+) ([0-9]+) ([0-9]+) (IN) (IP4|IP6) ([^ ]+)$/); 112 | if (0 === matches.length) { 113 | Logger.log('\'o=\' (Origin) formatted incorrectly: ' + line); 114 | return false; 115 | } 116 | 117 | this.origin = new Object(); 118 | this.origin.username = matches[1]; 119 | this.origin.sessionid = matches[2]; 120 | this.origin.sessionversion = matches[3]; 121 | this.origin.nettype = matches[4]; 122 | this.origin.addresstype = matches[5]; 123 | this.origin.unicastaddress = matches[6]; 124 | 125 | return true; 126 | } 127 | 128 | private function parseSessionName(line:String):Boolean { 129 | var matches:Array = line.match(/^s=([^\r\n]+)$/); 130 | if (0 === matches.length) { 131 | Logger.log('\'s=\' (Session Name) formatted incorrectly: ' + line); 132 | return false; 133 | } 134 | 135 | this.sessionName = matches[1]; 136 | 137 | return true; 138 | } 139 | 140 | private function parseTiming(line:String):Boolean { 141 | var matches:Array = line.match(/^t=([0-9]+) ([0-9]+)$/); 142 | if (0 === matches.length) { 143 | Logger.log('\'t=\' (Timing) formatted incorrectly: ' + line); 144 | return false; 145 | } 146 | 147 | this.timing = new Object(); 148 | timing.start = matches[1]; 149 | timing.stop = matches[2]; 150 | 151 | return true; 152 | } 153 | 154 | private function parseMediaDescription(line:String, media:Object):Boolean { 155 | var matches:Array = line.match(/^m=([^ ]+) ([^ ]+) ([^ ]+)[ ]/); 156 | if (0 === matches.length) { 157 | Logger.log('\'m=\' (Media) formatted incorrectly: ' + line); 158 | return false; 159 | } 160 | 161 | media.type = matches[1]; 162 | media.port = matches[2]; 163 | media.proto = matches[3]; 164 | media.fmt = line.substr(matches[0].length).split(' ').map(function(fmt:*, index:int, array:Array):int { 165 | return parseInt(fmt); 166 | }); 167 | 168 | return true; 169 | } 170 | 171 | private function parseAttribute(line:String, media:Object):Boolean { 172 | if (null === media) { 173 | /* Not in a media block, can't be bothered parsing attributes for session */ 174 | return true; 175 | } 176 | 177 | var matches:Array; /* Used for some cases of below switch-case */ 178 | var separator:int = line.indexOf(':'); 179 | var attribute:String = line.substr(0, (-1 === separator) ? 0x7FFFFFFF : separator); /* 0x7FF.. is default */ 180 | 181 | switch (attribute) { 182 | case 'a=recvonly': 183 | case 'a=sendrecv': 184 | case 'a=sendonly': 185 | case 'a=inactive': 186 | media.mode = line.substr('a='.length); 187 | break; 188 | 189 | case 'a=control': 190 | media.control = line.substr('a=control:'.length); 191 | break; 192 | 193 | case 'a=rtpmap': 194 | matches = line.match(/^a=rtpmap:(\d+) (.*)$/); 195 | if (null === matches) { 196 | Logger.log('Could not parse \'rtpmap\' of \'a=\''); 197 | return false; 198 | } 199 | 200 | var payload:int = parseInt(matches[1]); 201 | media.rtpmap[payload] = new Object(); 202 | 203 | var attrs:Array = matches[2].split('/'); 204 | media.rtpmap[payload].name = attrs[0]; 205 | media.rtpmap[payload].clock = attrs[1]; 206 | if (undefined !== attrs[2]) { 207 | media.rtpmap[payload].encparams = attrs[2]; 208 | } 209 | 210 | break; 211 | 212 | case 'a=fmtp': 213 | matches = line.match(/^a=fmtp:(\d+) (.*)$/); 214 | if (0 === matches.length) { 215 | Logger.log('Could not parse \'fmtp\' of \'a=\''); 216 | return false; 217 | } 218 | 219 | media.fmtp = new Object(); 220 | for each (var param:String in matches[2].split(';')) { 221 | var idx:int = param.indexOf('='); 222 | media.fmtp[StringUtil.trim(param.substr(0, idx).toLowerCase())] = StringUtil.trim(param.substr(idx + 1)); 223 | } 224 | 225 | break; 226 | case 'a=range': 227 | media.range = line.substr('a=range:'.length); 228 | break 229 | } 230 | 231 | return true; 232 | } 233 | 234 | public function getSessionBlock():Object { 235 | return this.sessionBlock; 236 | } 237 | 238 | public function getMediaBlock(mediaType:String):Object { 239 | return this.media[mediaType]; 240 | } 241 | 242 | public function getMediaBlockByPayloadType(pt:int):Object { 243 | for each (var m:Object in this.media) { 244 | if (-1 !== m.fmt.indexOf(pt)) { 245 | return m; 246 | } 247 | } 248 | 249 | ErrorManager.dispatchError(826, [pt], true); 250 | 251 | return null; 252 | } 253 | 254 | public function getMediaBlockList():Array { 255 | var res:Array = []; 256 | for each (var m:Object in this.media) { 257 | res.push(m); 258 | } 259 | 260 | return res; 261 | } 262 | } 263 | } 264 | -------------------------------------------------------------------------------- /src/com/axis/mjpegclient/Handle.as: -------------------------------------------------------------------------------- 1 | package com.axis.mjpegclient { 2 | 3 | import com.axis.mjpegclient.Image; 4 | 5 | import com.axis.rtspclient.ByteArrayUtils; 6 | import flash.events.ErrorEvent; 7 | import flash.events.Event; 8 | import flash.events.EventDispatcher; 9 | import flash.events.IOErrorEvent; 10 | import flash.events.ProgressEvent; 11 | import flash.events.SecurityErrorEvent; 12 | import flash.net.Socket; 13 | import flash.utils.ByteArray; 14 | import flash.events.TimerEvent; 15 | import flash.utils.Timer; 16 | 17 | import com.axis.Logger; 18 | import com.axis.ErrorManager; 19 | import com.axis.http.request; 20 | import com.axis.http.auth; 21 | 22 | [Event(name="connect",type="flash.events.Event")] 23 | [Event(name="error",type="flash.events.Event")] 24 | 25 | public class Handle extends EventDispatcher { 26 | 27 | public static const CONNECTED:String = "connected"; 28 | public static const CLOSED:String = "closed"; 29 | 30 | private var urlParsed:Object; 31 | private var socket:Socket = null; 32 | private var bcTimer:Timer; 33 | private var buffer:ByteArray = null; 34 | private var dataBuffer:ByteArray = null; 35 | private var url:String = ""; 36 | private var params:Object; 37 | private var parseHeaders:Boolean = true; 38 | private var headers:Vector. = new Vector.(); 39 | private var clen:int; 40 | private var parseSubheaders:Boolean = true; 41 | 42 | private var firstTimestamp:Number = -1; 43 | private var closeTiggered:Boolean = false; 44 | 45 | private var authState:String = "none"; 46 | private var authOpts:Object = {}; 47 | private var digestNC:uint = 1; 48 | private var method:String = ""; 49 | 50 | public var image:ByteArray = null; 51 | 52 | public function Handle(urlParsed:Object) { 53 | this.urlParsed = urlParsed; 54 | this.buffer = new ByteArray(); 55 | this.dataBuffer = new ByteArray(); 56 | this.socket = new Socket(); 57 | this.socket.timeout = 5000; 58 | this.socket.addEventListener(Event.CONNECT, onConnect); 59 | // If the close event is called we need to call socket.close() to prevent 60 | // security error 61 | this.socket.addEventListener(Event.CLOSE, onClose); 62 | this.socket.addEventListener(ProgressEvent.SOCKET_DATA, onHttpHeaders); 63 | this.socket.addEventListener(IOErrorEvent.IO_ERROR, onError); 64 | this.socket.addEventListener(SecurityErrorEvent.SECURITY_ERROR, onError); 65 | } 66 | 67 | private function onError(e:ErrorEvent):void { 68 | disconnect(); 69 | dispatchEvent(new Event("IOError:" + e.errorID)); 70 | } 71 | 72 | public function disconnect():void { 73 | Logger.log('MJPEGClient: disconnecting from', urlParsed.host + ':' + urlParsed.port); 74 | dispatchEvent(new Event(Event.CLOSE)); 75 | } 76 | 77 | public function connect():void { 78 | if (socket.connected) { 79 | disconnect(); 80 | } 81 | 82 | this.bcTimer = new Timer(Player.config.connectionTimeout * 1000, 1); 83 | this.bcTimer.stop(); // Don't start timeout immediately 84 | this.bcTimer.reset(); 85 | this.bcTimer.addEventListener(TimerEvent.TIMER_COMPLETE, bcTimerHandler); 86 | 87 | Logger.log('MJPEGClient: connecting to', urlParsed.host + ':' + urlParsed.port); 88 | socket.connect(urlParsed.host, urlParsed.port); 89 | } 90 | 91 | private function onConnect(event:Event):void { 92 | Logger.log('MJPEGClient: requesting URL', urlParsed.urlpath); 93 | this.bcTimer.start(); 94 | buffer = new ByteArray(); 95 | dataBuffer = new ByteArray(); 96 | 97 | dispatchEvent(new Event(Handle.CONNECTED)); 98 | 99 | headers.length = 0; 100 | parseHeaders = true; 101 | parseSubheaders = true; 102 | 103 | var authHeader:String = 104 | auth.authorizationHeader(method, authState, authOpts, urlParsed, digestNC++); 105 | 106 | socket.writeUTFBytes("GET " + urlParsed.urlpath + " HTTP/1.0\r\n"); 107 | socket.writeUTFBytes("Host: " + urlParsed.host + ':' + urlParsed.port + "\r\n"); 108 | socket.writeUTFBytes(authHeader); 109 | socket.writeUTFBytes("Accept: multipart/x-mixed-replace\r\n"); 110 | socket.writeUTFBytes("User-Agent: Locomote\r\n"); 111 | socket.writeUTFBytes("\r\n"); 112 | } 113 | 114 | private function onClose(e:Event):void { 115 | Logger.log('MJPEGClient: socket closed', urlParsed.host + ':' + urlParsed.port); 116 | 117 | if (this.bcTimer) { 118 | this.bcTimer.stop(); 119 | this.bcTimer.removeEventListener(TimerEvent.TIMER_COMPLETE, bcTimerHandler); 120 | this.bcTimer = null; 121 | } 122 | 123 | this.buffer = null 124 | 125 | try { 126 | // Security error is thrown if this line is excluded 127 | socket.close(); 128 | } catch (error:*) {} 129 | 130 | if (!this.closeTiggered) { 131 | dispatchEvent(new Event(Handle.CLOSED)); 132 | this.closeTiggered = true; 133 | } 134 | } 135 | 136 | private function onHttpHeaders(event:ProgressEvent):void { 137 | this.bcTimer.reset(); 138 | 139 | socket.readBytes(dataBuffer, dataBuffer.length); 140 | 141 | var parsed:* = request.readHeaders(socket, dataBuffer); 142 | if (false === parsed) { 143 | return; 144 | } 145 | 146 | Logger.log('MJPEGClient: recieved HTTP headers.', { 147 | httpCode: parsed.code, 148 | contentType: parsed.headers['content-type'], 149 | url: urlParsed.urlpath 150 | }); 151 | 152 | if (401 === parsed.code) { 153 | Logger.log('Unauthorized using auth method: ' + authState); 154 | /* Unauthorized, change authState and (possibly) try again */ 155 | authOpts = parsed.headers['www-authenticate']; 156 | var newAuthState:String = auth.nextMethod(authState, authOpts); 157 | if (authState === newAuthState) { 158 | ErrorManager.dispatchError(parsed.code); 159 | return; 160 | } 161 | 162 | Logger.log('switching http-authorization from ' + authState + ' to ' + newAuthState); 163 | authState = newAuthState; 164 | connect(); 165 | return; 166 | } 167 | 168 | if (200 !== parsed.code) { 169 | ErrorManager.dispatchError(parsed.code); 170 | disconnect(); 171 | return; 172 | } 173 | 174 | if (!/^multipart\/x-mixed-replace/.test(parsed.headers['content-type'])) { 175 | ErrorManager.dispatchError(829); 176 | disconnect(); 177 | return; 178 | } 179 | 180 | this.socket.removeEventListener(ProgressEvent.SOCKET_DATA, onHttpHeaders); 181 | this.socket.addEventListener(ProgressEvent.SOCKET_DATA, onImageData); 182 | 183 | // Remove HTTP header data from buffer as it is already parsed. 184 | var tmp:ByteArray = new ByteArray(); 185 | dataBuffer.readBytes(tmp); 186 | dataBuffer.clear(); 187 | dataBuffer = tmp; 188 | 189 | if (0 < this.dataBuffer.bytesAvailable) { 190 | this.onImageData(event); 191 | } 192 | } 193 | 194 | private function onImageData(event:ProgressEvent):void { 195 | this.bcTimer.reset(); 196 | socket.readBytes(dataBuffer, dataBuffer.length); 197 | 198 | if (parseSubheaders) { 199 | var parsed:* = request.readHeaders(socket, dataBuffer); 200 | if (false === parsed) { 201 | return; 202 | } 203 | 204 | this.clen = parsed.headers['content-length']; 205 | 206 | parseSubheaders = false; 207 | } 208 | 209 | findImage(); 210 | 211 | if (this.clen < this.dataBuffer.bytesAvailable) { 212 | onImageData(event); 213 | } 214 | }; 215 | 216 | private function findImage():void { 217 | if (this.dataBuffer.bytesAvailable < this.clen + 2) { 218 | return; 219 | } 220 | 221 | var image:ByteArray = new ByteArray() 222 | dataBuffer.readBytes(image, 0, clen + 2); 223 | 224 | var copy:ByteArray = new ByteArray(); 225 | dataBuffer.readBytes(copy, 0); 226 | dataBuffer = copy; 227 | 228 | var timestamp:Number = new Date().getTime(); 229 | if (this.firstTimestamp === -1) { 230 | this.firstTimestamp = timestamp; 231 | } 232 | 233 | dispatchEvent(new Image(image, timestamp - this.firstTimestamp)); 234 | clen = 0; 235 | parseSubheaders = true; 236 | } 237 | 238 | private function bcTimerHandler(e:TimerEvent):void { 239 | Logger.log('MJPEGClient: connection timedout', urlParsed.host + ':' + urlParsed.port); 240 | this.disconnect(); 241 | } 242 | } 243 | } 244 | -------------------------------------------------------------------------------- /src/com/axis/ErrorManager.as: -------------------------------------------------------------------------------- 1 | package com.axis { 2 | import flash.display.LoaderInfo; 3 | import flash.external.ExternalInterface; 4 | 5 | public class ErrorManager { 6 | public static const STREAM_ERRORS:Object = { 7 | '100': "Continue", 8 | '200': "OK", 9 | '201': "Created", 10 | '250': "Low on Storage Space", 11 | '300': "Multiple Choices", 12 | '301': "Moved Permanently", 13 | '302': "Moved Temporarily", 14 | '303': "See Other", 15 | '304': "Not Modified", 16 | '305': "Use Proxy", 17 | '400': "Bad Request", 18 | '401': "Unauthorized", 19 | '402': "Payment Required", 20 | '403': "Forbidden", 21 | '404': "Not Found", 22 | '405': "Method Not Allowed", 23 | '406': "Not Acceptable", 24 | '407': "Proxy Authentication Required", 25 | '408': "Request Time-out", 26 | '410': "Gone", 27 | '411': "Length Required", 28 | '412': "Precondition Failed", 29 | '413': "Request Entity Too Large", 30 | '414': "Request-URI Too Large", 31 | '415': "Unsupported Media Type", 32 | '451': "Parameter Not Understood", 33 | '452': "Conference Not Found", 34 | '453': "Not Enough Bandwidth", 35 | '454': "Session Not Found", 36 | '455': "Method Not Valid in This State", 37 | '456': "Header Field Not Valid for Resource", 38 | '457': "Invalid Range", 39 | '458': "Parameter Is Read-Only", 40 | '459': "Aggregate operation not allowed", 41 | '460': "Only aggregate operation allowed", 42 | '461': "Unsupported transport", 43 | '462': "Destination unreachable", 44 | '463': "Key management Failure", 45 | '500': "Internal Server Error", 46 | '501': "Not Implemented", 47 | '502': "Bad Gateway", 48 | '503': "Service Unavailable", 49 | '504': "Gateway Time-out", 50 | '505': "RTSP Version not supported", 51 | '551': "Option not supported", 52 | '700': "NetConnection.Call.BadVersion - Packet encoded in an unidentified format.", 53 | '701': "NetConnection.Call.Failed - The NetConnection.call() method was not able to invoke the server-side method or command.", 54 | '702': "NetConnection.Call.Prohibited - An Action Message Format (AMF) operation is prevented for security reasons. Either the AMF URL is not in the same domain as the file containing the code calling the NetConnection.call() method, or the AMF server does not have a policy file that trusts the domain of the the file containing the code calling the NetConnection.call() method.", 55 | '703': "NetConnection.Connect.AppShutdown - The server-side application is shutting down.", 56 | '704': "NetConnection.Connect.Failed - The connection attempt failed.", 57 | '705': "NetConnection.Connect.InvalidApp - The application name specified in the call to NetConnection.connect() is invalid.", 58 | '706': "NetConnection.Connect.Rejected - The connection attempt did not have permission to access the application.", 59 | '707': "NetGroup.Connect.Failed - The NetGroup connection attempt failed. The info.group property indicates which NetGroup failed.", 60 | '708': "NetGroup.Connect.Rejected - The NetGroup is not authorized to function. The info.group property indicates which NetGroup was denied.", 61 | '709': "NetStream.Connect.Failed - The P2P connection attempt failed. The info.stream property indicates which stream has failed. Note: Not supported in AIR 3.0 for iOS.", 62 | '710': "NetStream.Connect.Rejected - The P2P connection attempt did not have permission to access the other peer. The info.stream property indicates which stream was rejected. Note: Not supported in AIR 3.0 for iOS.", 63 | '711': "NetStream.Failed - (Flash Media Server) An error has occurred for a reason other than those listed in other event codes.", 64 | '712': "NetStream.Play.Failed - An error has occurred in playback for a reason other than those listed elsewhere in this table, such as the subscriber not having read access. Note: Not supported in AIR 3.0 for iOS.", 65 | '713': "NetStream.Play.FileStructureInvalid - (AIR and Flash Player 9.0.115.0) The application detects an invalid file structure and will not try to play this type of file. Note: Not supported in AIR 3.0 for iOS.", 66 | '714': "NetStream.Play.InsufficientBW - (Flash Media Server) The client does not have sufficient bandwidth to play the data at normal speed. Note: Not supported in AIR 3.0 for iOS.", 67 | '715': "NetStream.Play.StreamNotFound - The file passed to the NetStream.play() method can't be found.", 68 | '716': "NetStream.Publish.BadName - Attempt to publish a stream which is already being published by someone else.", 69 | '717': "NetStream.Record.Failed - An attempt to record a stream failed.", 70 | '718': "NetStream.Record.NoAccess - Attempt to record a stream that is still playing or the client has no access right.", 71 | '719': "NetStream.Seek.Failed - The seek fails, which happens if the stream is not seekable.", 72 | '720': "NetStream.Seek.InvalidTime - For video downloaded progressively, the user has tried to seek or play past the end of the video data that has downloaded thus far, or past the end of the video once the entire file has downloaded. The info.details property of the event object contains a time code that indicates the last valid position to which the user can seek.", 73 | '721': "SharedObject.BadPersistence - A request was made for a shared object with persistence flags, but the request cannot be granted because the object has already been created with different flags.", 74 | '722': "SharedObject.Flush.Failed - The 'pending' status is resolved, but the SharedObject.flush() failed.", 75 | '723': "SharedObject.UriMismatch - The video dimensions are available or have changed. Use the Video or StageVideo videoWidth/videoHeight property to query the new video dimensions. New in Flash Player 11.4/AIR 3.4.", 76 | '724': "Unknown NetStatus error: %p", 77 | '725': "NetStream reported an asyncError: %p", 78 | '726': "NetStream reported a DRMError with ID: %p and subID: %p.", 79 | '727': "NetStream reported an IOError: %p.", 80 | '728': "NetConnection reported an asyncError.", 81 | '729': "NetConnection reported an IOError: %p.", 82 | '730': "NetConnection reported a security error: %p.", 83 | '731': "Socket reported a security error: %p.", 84 | '732': "Socket reported an IOError: %p.", 85 | '800': "Unable to pause a stream if not playing.", 86 | '801': "Unable to resume a stream if not paused.", 87 | '802': "################### Unused", 88 | '803': "RTSPClient: Handle unexpectedly closed.", 89 | '804': "Unknown determining byte: 0x%p. Stopping stream.", 90 | '805': "Cannot start unless in initial state.", 91 | '806': "RTSPClient:Failed to parse SDP file.", 92 | '807': "No tracks in SDP file.", 93 | '808': "Unable to pause. This might not work for this stream-type, or this particular stream.", 94 | '809': "Unable to resume. This might not work for this stream-type, or this particular stream.", 95 | '810': "Unable to stop. This might not work for this stream-type, or this particular stream.", 96 | '811': "Unknown streaming protocol: %p", 97 | '812': "Unsupported audio transmit protocol.", 98 | '813': "Denied access to microphone.", 99 | '814': "Already connected to microphone.", 100 | '815': "No audio transmit url provided.", 101 | '816': "Denied access to microphone.", 102 | '817': "Audio transmit already connected.", 103 | '818': "No audio transmit url provided.", 104 | '819': "BitArray: Bit ranges must be 1 - 32.", 105 | '820': "BitArray: exp-golomb larger than 32 bits is unsupported.", 106 | '821': "ByteArray: Unsupported Pattern", 107 | '822': "FLVMux: No support for Chroma/Luma scaling matrix", 108 | '823': "FLVMux: No support for parsing 'pic_order_cnt_type' != 0", 109 | '824': "RTSPClient: Unable to determine control URL.", 110 | '825': "RTSPClient: Pause is not supported by server.", 111 | '826': "No media block for payload type: %p", 112 | '827': "Connection broken. The stream has been stopped.", 113 | '828': "Unable to seek.", 114 | '829': "httpm only supports Content-Type: 'multipart/x-mixed-replace'", 115 | '830': "Unable to set buffer. This might not work for this stream-type, or this particular stream.", 116 | '831': "Unsupported Audio or Video format: %p", 117 | '832': "Unable to set frame by frame. This might not work for this stream-type, or this particular stream.", 118 | '833': "Failed to load mjpeg image.", 119 | '834': "Unable to set keep alive interval" 120 | }; 121 | 122 | public static function dispatchError(errorCode:Number, errorData:Array = null, throwError:Boolean = false):void { 123 | var functionName:String = "LocomoteMap['" + Player.locomoteID + "'].__playerEvent"; 124 | var errorMessage:String = (errorData) ? ErrorManager.resolveErrorString(STREAM_ERRORS[errorCode], errorData) : STREAM_ERRORS[errorCode]; 125 | if (null === errorMessage) { 126 | errorMessage = "An unknown error has occurred."; 127 | } 128 | var errorInfo:Object = { 129 | 'code': errorCode, 130 | 'message': errorMessage 131 | }; 132 | ExternalInterface.call(functionName, "error", errorInfo); 133 | if (throwError) { 134 | throw new Error(errorMessage); 135 | } 136 | } 137 | 138 | public static function resolveErrorString(errorString:String, errorData:Array):String { 139 | var pattern:RegExp = /%p/; 140 | errorData.forEach(function(item:*, i:int, arr:Array):void { 141 | errorString = errorString.replace(pattern, item); 142 | }); 143 | 144 | return errorString; 145 | } 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /src/com/axis/audioclient/AxisTransmit.as: -------------------------------------------------------------------------------- 1 | package com.axis.audioclient { 2 | import com.axis.audioclient.IAudioClient; 3 | import com.axis.codec.g711; 4 | import com.axis.ErrorManager; 5 | import com.axis.http.auth; 6 | import com.axis.http.request; 7 | import com.axis.http.url; 8 | import com.axis.Logger; 9 | 10 | import flash.events.ErrorEvent; 11 | import flash.events.Event; 12 | import flash.events.IOErrorEvent; 13 | import flash.events.ProgressEvent; 14 | import flash.events.SampleDataEvent; 15 | import flash.events.SecurityErrorEvent; 16 | import flash.events.StatusEvent; 17 | import flash.external.ExternalInterface; 18 | import flash.media.Microphone; 19 | import flash.media.SoundCodec; 20 | import flash.net.Socket; 21 | import flash.net.SecureSocket; 22 | import flash.utils.ByteArray; 23 | 24 | public class AxisTransmit implements IAudioClient { 25 | private static const EVENT_AUDIO_TRANSMIT_STARTED:String = 'audioTransmitStarted'; 26 | private static const EVENT_AUDIO_TRANSMIT_STOPPED:String = 'audioTransmitStopped'; 27 | private static const EVENT_AUDIO_TRANSMIT_REQUEST_PERMISSION:String = 'audioTransmitRequestPermission'; 28 | private static const EVENT_AUDIO_TRANSMIT_ALLOWED:String = 'audioTransmitAllowed'; 29 | private static const EVENT_AUDIO_TRANSMIT_DENIED:String = 'audioTransmitDenied'; 30 | 31 | private var urlParsed:Object = {}; 32 | private var conn:Socket; 33 | 34 | private var authState:String = 'none'; 35 | private var authOpts:Object = {}; 36 | 37 | private var savedUrl:String = null; 38 | 39 | private var mic:Microphone; 40 | private var _microphoneVolume:Number; 41 | private var permissionResovled:Boolean = false; 42 | 43 | private var currentState:String = 'stopped'; 44 | 45 | public function AxisTransmit() { 46 | /* Set default microphone volume */ 47 | this.microphoneVolume = 50; 48 | } 49 | 50 | private function onMicSampleDummy(event:SampleDataEvent):void {} 51 | 52 | private function onMicStatus(event:StatusEvent):void { 53 | Logger.log("AxisTransmit: MicStatus", { event: event.code }); 54 | 55 | this.permissionResovled = true; 56 | this.mic.removeEventListener(StatusEvent.STATUS, onMicStatus); 57 | this.mic.removeEventListener(SampleDataEvent.SAMPLE_DATA, onMicSampleDummy); 58 | 59 | switch (event.code) { 60 | case 'Microphone.Muted': 61 | this.callAPI(EVENT_AUDIO_TRANSMIT_DENIED); 62 | break; 63 | case 'Microphone.Unmuted': 64 | this.callAPI(EVENT_AUDIO_TRANSMIT_ALLOWED); 65 | break; 66 | } 67 | } 68 | 69 | public function start(iurl:String = null):void { 70 | if (conn && conn.connected) { 71 | ErrorManager.dispatchError(817); 72 | return; 73 | } 74 | 75 | this.currentState = 'initial'; 76 | 77 | var currentUrl:String = (iurl) ? iurl : savedUrl; 78 | 79 | if (!currentUrl) { 80 | ErrorManager.dispatchError(818); 81 | return; 82 | } 83 | 84 | this.urlParsed = url.parse(currentUrl); 85 | this.savedUrl = currentUrl; 86 | 87 | this.mic = Microphone.getMicrophone(); 88 | 89 | if (null === this.mic) { 90 | ErrorManager.dispatchError(819); 91 | return; 92 | } 93 | 94 | this.mic.rate = 16; 95 | this.mic.setSilenceLevel(0, -1); 96 | 97 | conn = this.urlParsed.protocol == 'https' ? new SecureSocket() : new Socket(); 98 | conn.addEventListener(Event.CONNECT, onConnected); 99 | conn.addEventListener(Event.CLOSE, onClosed); 100 | conn.addEventListener(ProgressEvent.SOCKET_DATA, onRequestData); 101 | conn.addEventListener(IOErrorEvent.IO_ERROR, onIOError); 102 | conn.addEventListener(SecurityErrorEvent.SECURITY_ERROR, onSecurityError); 103 | 104 | if (this.mic.muted) { 105 | if (this.permissionResovled) { 106 | ErrorManager.dispatchError(816); 107 | } else { 108 | this.mic.addEventListener(StatusEvent.STATUS, onMicStatus); 109 | this.mic.addEventListener(SampleDataEvent.SAMPLE_DATA, onMicSampleDummy); 110 | this.callAPI(EVENT_AUDIO_TRANSMIT_REQUEST_PERMISSION); 111 | } 112 | } else { 113 | this.connect(); 114 | } 115 | } 116 | 117 | public function stop():void { 118 | this.close(); 119 | } 120 | 121 | private function connect():void { 122 | if (!conn.connected) { 123 | Logger.log("AxisTransmit: Connecting to ", this.urlParsed.host + ":" + this.urlParsed.port); 124 | conn.connect(this.urlParsed.host, this.urlParsed.port); 125 | } 126 | } 127 | 128 | private function onConnected(event:Event):void { 129 | Logger.log("AxisTransmit: Connected to ", this.urlParsed.host + ":" + this.urlParsed.port); 130 | 131 | this.mic.addEventListener(SampleDataEvent.SAMPLE_DATA, onMicSampleData); 132 | 133 | conn.writeUTFBytes("POST " + this.urlParsed.urlpath + " HTTP/1.0\r\n"); 134 | conn.writeUTFBytes("Content-Type: audio/axis-mulaw-128\r\n"); 135 | conn.writeUTFBytes("Content-Length: 9999999\r\n"); 136 | conn.writeUTFBytes("Connection: Keep-Alive\r\n"); 137 | conn.writeUTFBytes("Cache-Control: no-cache\r\n"); 138 | writeAuthorizationHeader(); 139 | conn.writeUTFBytes("\r\n"); 140 | } 141 | 142 | private function close():void { 143 | if (conn.connected) { 144 | Logger.log("AxisTransmit: Disconnecting from ", this.urlParsed.host + ":" + this.urlParsed.port); 145 | conn.close(); 146 | this.onClosed(); 147 | } 148 | } 149 | 150 | private function onClosed(event:Event = null):void { 151 | Logger.log("AxisTransmit: Disconnected from ", this.urlParsed.host + ":" + this.urlParsed.port); 152 | 153 | this.mic.removeEventListener(SampleDataEvent.SAMPLE_DATA, onMicSampleData); 154 | 155 | if ('playing' === this.currentState) { 156 | this.currentState = 'stopped'; 157 | this.callAPI(EVENT_AUDIO_TRANSMIT_STOPPED); 158 | } 159 | } 160 | 161 | private function onMicSampleData(event:SampleDataEvent):void { 162 | if ('playing' !== this.currentState) { 163 | this.currentState = 'playing'; 164 | this.callAPI(EVENT_AUDIO_TRANSMIT_STARTED); 165 | } 166 | 167 | while (event.data.bytesAvailable) { 168 | var encoded:uint = g711.linearToMulaw(event.data.readFloat()); 169 | conn.writeByte(encoded); 170 | } 171 | 172 | conn.flush(); 173 | } 174 | 175 | private function onRequestData(event:ProgressEvent):void { 176 | var data:ByteArray = new ByteArray(); 177 | var parsed:* = request.readHeaders(conn, data); 178 | 179 | if (false === parsed) { 180 | return; 181 | } 182 | 183 | if (401 === parsed.code) { 184 | /* Unauthorized, change authState and (possibly) try again */ 185 | authOpts = parsed.headers['www-authenticate']; 186 | var newAuthState:String = auth.nextMethod(authState, authOpts); 187 | 188 | if (authState === newAuthState) { 189 | ErrorManager.dispatchError(parsed.code); 190 | authState = 'none'; 191 | return; 192 | } 193 | 194 | Logger.log('AxisTransmit: switching http-authorization from ' + authState + ' to ' + newAuthState); 195 | authState = newAuthState; 196 | this.close(); 197 | this.connect(); 198 | return; 199 | } 200 | } 201 | 202 | private function writeAuthorizationHeader():void { 203 | var a:String = ''; 204 | switch (authState) { 205 | case "basic": 206 | a = auth.basic(this.urlParsed.user, this.urlParsed.pass) + "\r\n"; 207 | break; 208 | 209 | case "digest": 210 | a = auth.digest( 211 | this.urlParsed.user, 212 | this.urlParsed.pass, 213 | "POST", 214 | authOpts.digestRealm, 215 | urlParsed.urlpath, 216 | authOpts.qop, 217 | authOpts.nonce, 218 | 1 219 | ); 220 | break; 221 | 222 | default: 223 | case "none": 224 | return; 225 | } 226 | 227 | conn.writeUTFBytes('Authorization: ' + a + "\r\n"); 228 | } 229 | 230 | private function onIOError(event:IOErrorEvent):void { 231 | ErrorManager.dispatchError(732, [event.text]); 232 | } 233 | 234 | private function onSecurityError(event:SecurityErrorEvent):void { 235 | ErrorManager.dispatchError(731, [event.text]); 236 | } 237 | 238 | public function get microphoneVolume():Number { 239 | return _microphoneVolume; 240 | } 241 | 242 | public function set microphoneVolume(volume:Number):void { 243 | if (null === mic) { 244 | ErrorManager.dispatchError(819); 245 | return; 246 | } 247 | 248 | _microphoneVolume = volume; 249 | mic.gain = volume; 250 | 251 | if (volume && savedUrl) 252 | start(); 253 | } 254 | 255 | public function muteMicrophone():void { 256 | if (null === mic) { 257 | ErrorManager.dispatchError(819); 258 | return; 259 | } 260 | 261 | mic.gain = 0; 262 | stop(); 263 | } 264 | 265 | public function unmuteMicrophone():void { 266 | if (null === mic) { 267 | ErrorManager.dispatchError(819); 268 | return; 269 | } 270 | 271 | if (mic.gain !== 0) 272 | return; 273 | 274 | mic.gain = this.microphoneVolume; 275 | start(); 276 | } 277 | 278 | private function callAPI(eventName:String, data:Object = null):void { 279 | var functionName:String = "LocomoteMap['" + Player.locomoteID + "'].__playerEvent"; 280 | if (data) { 281 | ExternalInterface.call(functionName, eventName, data); 282 | } else { 283 | ExternalInterface.call(functionName, eventName); 284 | } 285 | } 286 | } 287 | } 288 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Locomote Video Player [![Build Status](https://travis-ci.org/AxisCommunications/locomote-video-player.svg?branch=master)](https://travis-ci.org/AxisCommunications/locomote-video-player) [![Gitter](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/AxisCommunications/locomote-video-player?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) 2 | 3 | # END-OF-LIFE NOTICE 4 | As [Adobe Flash is reaching its end-of-life](https://theblog.adobe.com/adobe-flash-update/) Axis Communications is no longer maintaining this project. 5 | 6 | For native HTML5/JavaScript video streaming of Axis cameras, see https://github.com/AxisCommunications/media-stream-library-js instead. 7 | 8 | ## Getting started 9 | 10 | ### Installing Locomote 11 | Install Locomote using [Bower](http://bower.io) with the following command: 12 | 13 | ``` 14 | bower install locomote 15 | ``` 16 | 17 | Install Locomote using [npm](http://npmjs.com) with the following command: 18 | ``` 19 | npm install locomote-video-player 20 | ``` 21 | 22 | ### Running Locomote 23 | 24 | To run Locomote in a web page, you need to host both the SWF (`Player.swf` in example below), 25 | and the JavaScript library (`locomote.min.js` in example below). Use a simple page like: 26 | 27 | ```html 28 | 29 | 30 | Locomote 31 | 37 | 38 | 39 | 65 | 66 | 67 |
68 | 69 | 70 | ``` 71 | 72 | ### Socket Policy Server 73 | 74 | `Locomote` uses sockets to connect to RTSP video streams which requires a socket policy server to be implemented. For RTMP and HTTP streams no socket policy server is required. 75 | 76 | Flash Player 9 and above implements a strict access policy for Flash applications that make Socket or XMLSocket connections to a remote host. It now requires the presence of a socket policy file on the server. 77 | 78 | When the Flash Player tries to make a connection, it checks in two places for the socket policy: 79 | 80 | - Port 843. If you are the administrator of a server, you can set up an application to listen on this port and return a server-wide socket policy. 81 | - The destination port. If you're running your own xml server, you can configure it to send the socket policy file. 82 | 83 | The Flash player always tries port 843 first; if there's no response after 3 seconds, then it tries the destination port. 84 | 85 | When the Flash player makes a connection, it sends the following XML string to the server: 86 | 87 | ``` 88 | 89 | ``` 90 | 91 | Your server then must send the following XML in reply: 92 | 93 | ``` 94 | 95 | 96 | 97 | ``` 98 | 99 | `*` is the wildcard and means "all ports/domains". If you want to restrict access to a particular port, enter the port number, or a list or range of numbers. 100 | 101 | For more info about socket policy files and how to set up a server please read the following articles: 102 | 103 | [Setting up a socket policy file server][SocketPolicySetup] 104 | 105 | [Policy file changes in Flash Player 9 and Flash Player 10][SocketPolicyChanges] 106 | 107 | ## API Specification 108 | 109 | ### Construction / Destruction 110 | 111 | #### Locomote(element, url) 112 | 113 | > Locomote player constructor. Will load the Locomote SWF and embed the Locomote player in a DOM element. 114 | 115 | > First argument is either an ID to an element in the DOM as a string or a reference to a DOM element. This is where Locomote will embed the player. 116 | 117 | > The second argument is the URL to the player SWF. 118 | 119 | > The player will load asynchronously. When the player is loaded an `apiReady` event is sent. Before the `apiReady` event, no API methods can be used except `on` and `off`. 120 | 121 | #### destroy() 122 | 123 | > Will remove the tag from the element is was embedded to and remove all references 124 | > to it held by the javascript library. This can be called as any other action. E.g. 125 | > 126 | ```javascript 127 | var locomote = new Locomote('player', 'Player.swf'); 128 | locomote.destroy(); 129 | ``` 130 | 131 | ### Actions 132 | 133 | #### play(url:String, [options:Object]) 134 | 135 | > Starts playing video from url. Protocol is determined by url. 136 | > Example: `rtsp://server:port/stream`. 137 | > 138 | > Supported protocols: 139 | > 140 | > - `rtsp` - [RTSP over TCP][RTSP/TCP] 141 | > - `rtsph` - [RTSP over HTTP][RTSP/HTTP] 142 | > - `rtsphs` - [RTSP over HTTPS][RTSP/HTTP] 143 | > - `rtsphap` - RTSP over HTTPS via Axis Proxy 144 | > - `rtmp` - [RTMP][RTMP] 145 | > - `rtmpt` - RTMP over HTTP 146 | > - `rtmps` - RTMP over SSL 147 | > - `http` - Progressive download via HTTP 148 | > - `https` - Progressive download via HTTP over SSL 149 | > - `httpm` - [MJPEG over HTTP][MJPEG/HTTP] (via multipart/x-mixed-replace) 150 | 151 | > `options` is an optional object with the following attributes: 152 | > - `offset` - The offset to start the stream at. This is only supported by the 153 | `rtsp[h|hs|hap]` protocol and requires the RTSP server to respect the range 154 | header in the play request. 155 | > - `httpUrl` - The URL to use in HTTP requests if it differs from the RTSP 156 | URL. This is only supported by the `rtsp[h|hs]` (Note: not supported by `rtsphap`) protocol 157 | 158 | #### stop() 159 | 160 | > Stops video stream. 161 | 162 | #### seek(offset) 163 | 164 | > Seeks to the position specified by `offset` (calculated from the start of stream). 165 | > 166 | > If the currently player stream is RTMP, it may not work with seeking if the stream is live. Even if the material played is recorded 167 | > it may not work depending on RTMP server implementation. 168 | > In the RTMP case, this is really just delegated to [the implementation in the NetStream class][NetStream:seek]. 169 | > 170 | > This does not work for RTSP at all (yet). 171 | 172 | #### pause() 173 | 174 | > Pauses video stream. 175 | 176 | #### resume() 177 | 178 | > Resumes video from paused state. 179 | 180 | #### playFrames(timestamp) 181 | 182 | > Appends all received frames up to and including the given timestamp to the 183 | play buffer. Only applicable if player is configured with `frameByFrame`. 184 | 185 | #### streamStatus() 186 | 187 | > Returns a status object with the following data (if an entry is unknown, that value will be null): 188 | 189 | > - fps - frames per second. 190 | > - resolution (object) - the stream size `{ width, height }`. 191 | > - playbackSpeed - current playback speed. 1.0 is normal stream speed. 192 | > - current time - ms from start of stream. 193 | > - protocol - which high-level transport protocol is in use. 194 | > - state - current playback state (playing, paused, stopped). 195 | > - streamURL - the source of the current media. 196 | > - duration - the duration of the currently playing media, or -1 if not available 197 | 198 | #### playerStatus() 199 | 200 | > Returns a status object with the following data: 201 | 202 | > - buffer - The length of the buffer in seconds. 203 | > - microphoneVolume - the volume of the microphone when capturing audio 204 | > - speakerVolume - the volume of the speakers (i.e. the stream volume). 205 | > - microphoneMuted (bool) - if the microphone is muted. 206 | > - speakerMuted (bool) - if the speakers are muted. 207 | > - fullScreen (bool) - if the player is currently in fullscreen mode. 208 | > - version - the Locomote version number. 209 | 210 | #### speakerVolume(vol) 211 | 212 | > Sets speaker volume from 0-100. The default value is 50. 213 | 214 | #### muteSpeaker() 215 | 216 | > Mutes the speaker volume. Remembers the current volume and resets to it if the 217 | speakers are unmuted. 218 | 219 | #### unmuteSpeaker() 220 | 221 | > Resets the volume to previous unmuted value. 222 | 223 | #### microphoneVolume(vol) 224 | 225 | > Sets microphone volume from 0-100. The default value is 50. 226 | 227 | #### muteMicrophone() 228 | 229 | > Mutes the microphone. Remembers the current volume and resets to it if the 230 | microphone is unmuted. 231 | 232 | #### unmuteMicrophone() 233 | 234 | > Resets the volume to previous unmuted value. 235 | 236 | #### startAudioTransmit(url, type) 237 | 238 | > Starts transmitting microphone input to the camera speaker. 239 | The optional `type` parameter can be used for future implementations of other protocols, 240 | currently only the Axis audio transmit api is supported. 241 | For Axis cameras the `url` parameter should be in the format - `http://server:port/axis-cgi/audio/transmit.cgi`. 242 | 243 | > If the user must grant permission to use the microphone an 244 | `audioTransmitRequestPermission` event will be dispatched and 245 | `startAudioTransmit` must be called again once permission has been granted. 246 | 247 | #### stopAudioTransmit() 248 | 249 | > Stops transmitting microphone input to the camera speaker. 250 | 251 | #### config(config) 252 | 253 | > Sets configuration values of the player. `config` is a JavaScript object that can have the following optional values: 254 | 255 | > - `buffer` - The number of seconds that should be buffered. The default value is `3`. 256 | > - `connectionTimeout` - The number of seconds before a broken connection times out and is closed. The default value is `10`. 257 | > - `keepAlive` - The number of seconds between keep alive requests (only RTSP at the moment). The default value is `0` (disabled). 258 | > - `scaleUp` - Specifies if the video can be scaled up or not. The default value is `false`. 259 | > - `allowFullscreen` - Specifices if fullscreen mode is allowed or not. The default value is `true`. 260 | > - `debugLogger` - Specifices if debug messages should be shown in the Flash console or not. The default value is `false`. 261 | > - `frameByFrame` - Specifices if media should be played immediately or wait 262 | for calls to `playFrames`. Not supported by the `rtmp` protocol. The 263 | default value is `false`. The http and https protocol 264 | implements this by creating virtual frames, a timestamp 265 | given in the `frameReady` event may not correspond to a 266 | real video frame, and the player may play up to 50 ms more 267 | than the last `playFrames` call specified. The 268 | `rtsp[h|hs|hap]` protocol dispatches the `frameReady` event 269 | for each assembled FLV tag, if audio and video is received 270 | out of order this will cause `frameReady` events to be 271 | dispatched out of order. 272 | 273 | #### on(eventName:String, callback:Function) 274 | 275 | > Starts listening for events with `eventName`. Calls `callback` when event triggers. 276 | 277 | #### off(eventName:String, callback:Function) 278 | 279 | > Stops listening for events with eventName. 280 | 281 | ### Events 282 | 283 | #### apiReady 284 | 285 | > Dispatched when the player is fully initialized. This is always the first event to be sent. Before the `apiReady` event no API methods can be called except `on` and `off`. 286 | 287 | #### streamStarted 288 | 289 | > Dispatched when video streams starts. 290 | 291 | #### streamPaused(result) 292 | 293 | > Dispatched when video stream is paused. `result` is an object with a single property `reason` that can have the following values: 294 | 295 | > - `user` - stream was paused by user. 296 | > - `buffering` - stream has stopped for buffering. 297 | 298 | #### streamStopped 299 | 300 | > Dispatched when stream stops. 301 | 302 | #### frameReady(timestamp) 303 | 304 | > Dispatched when a new frame, or pseudo-frame, is available to be appended to 305 | the play buffer. The timestamp of the frame is given by the argument. Append 306 | it using the `playFrames` method. This event will only be dispatched if the 307 | player is configured with the `frameByFrame` option. Otherwise, all frames 308 | will be appended to the play buffer immediately when received and this event 309 | will not be dispatched. 310 | 311 | #### error(error) 312 | 313 | > Dispatched when video stream fails. `error` can be either 314 | > protocol error (rtsp etc) or Locomote internal error. 315 | > `error` is a generic object. 316 | 317 | > Locomote reports the following types of errors: 318 | > - `RTSP` - The default error codes that are sent from the RTSP stream. Error codes: 100 - 551. 319 | > - `Flash Player` - Errors that are reported by Flash Socket, NetStream and NetConnection classes. Error codes: 700 - 799. 320 | > - `Locomote` - Errors generated by the Locomote player. Error codes: 800 - 899. 321 | 322 | > For detailed information about the errors, please see the 323 | [ErrorManager][ErrorManager] class. 324 | 325 | #### audioTransmitStarted 326 | 327 | > Dispatched when audio transmission starts. 328 | 329 | #### audioTransmitStopped 330 | 331 | > Dispatched when audio transmission stops. 332 | 333 | #### audioTransmitRequestPermission 334 | 335 | > Dispatched when flash is prompting the user to grant or deny access to the 336 | microphone. When this event is dispatched the setup is aborted and 337 | `startAudioTransmit` must be called again after the event 338 | `audioTransmitAllowed` has been dispatched. 339 | 340 | #### audioTransmitAllowed 341 | 342 | > Dispatched when user has granted permission to use the microphone. A new call 343 | to `startAudioTransmit` must be made to initiate audio transmission. 344 | 345 | #### audioTransmitDenied 346 | 347 | > Dispatched when user has denied permission to use the microphone. If this 348 | event is fired, any future calls to `startAudioTransmit` will generate an 349 | error (816). 350 | 351 | #### fullscreenEntered 352 | 353 | > Dispatched when the player enters fullscreen mode. 354 | 355 | #### fullscreenExited 356 | 357 | > Dispatched when the player exits fullscreen mode. 358 | 359 | #### log 360 | 361 | > Dispatched when a log message is sent from the player. 362 | 363 | ## Building Locomote 364 | 365 | ### Building with npm 366 | 367 | To compile the project, [nodejs](http://www.nodejs.org) and [npm](http://www.npmjs.org) is required. 368 | Since `npm` is bundled with `nodejs`, you only need to download and install `nodejs`. 369 | 370 | To build `Locomote`, simply run `npm install` followed by `gulp` in the root directory. 371 | This will download [Adobe Flex SDK](http://www.adobe.com/devnet/flex/flex-sdk-download.html), 372 | and other required modules and build the `SWF` file and the `JavaScript` library to `dist/`. 373 | 374 | ### Building with Flash Builder 375 | 376 | It's also possible to build Locomote with Flash Builder. Follow the steps below to set up a Flash Builder project. 377 | 378 | - Clone the Locomote repository from Github. 379 | - Build the project with `npm` as described above. This will build as3corelib and the VERSION file which are both required dependencies. 380 | - Create a new ActionScript project from Flash Builder and save it in the root folder of the cloned repository. 381 | - Inside Flash Builder, right click the `Player.as` file that is now in the `default package` and select "Set as Default Application". 382 | - Remove the `.as` file with the same name you used for the project that was automatically created in `default package`. 383 | - Add as3corelib to the project by selecting "Properties" in the "Project menu" and then "ActionScript Build Path". Click "Add SWC..." and add as3corelib which is located here: `/ext/as3corelib/bin/as3corelib.swc`. Make sure that the library is merged into the code. Please note that the as3corelib.swc file will only be available after you have built the project with `npm`. 384 | - You may need to change the path to the default HTML file in "Run/Debug Settings". Edit the `Player` launch configuration and make sure that the correct url to the HTML file is selected. 385 | - The project can now be built by Flash Builder. Please note that you also need to modify the default HTML template provided with the Flash Builder project to load the swf and Javascript file properly. An example of a minimal HTML file is provided below. 386 | 387 | The Flash Builder project files and build folders will be ignored by git automatically so you shouldn't have to add anything to the repository after setting up the project. 388 | 389 | [SocketPolicySetup]: http://www.adobe.com/devnet/flashplayer/articles/socket_policy_files.html 390 | [SocketPolicyChanges]: http://www.adobe.com/devnet/flashplayer/articles/fplayer9_security.html 391 | [RTSP/TCP]: http://www.ietf.org/rfc/rfc2326.txt 392 | [RTSP/HTTP]: http://www.opensource.apple.com/source/QuickTimeStreamingServer/QuickTimeStreamingServer-412.42/Documentation/RTSP_Over_HTTP.pdf 393 | [RTMP]: http://www.adobe.com/devnet/rtmp.html 394 | [ErrorManager]: https://github.com/AxisCommunications/locomote-video-player/blob/master/src/com/axis/ErrorManager.as 395 | [NetStream:seek]: http://help.adobe.com/en_US/FlashPlatform/reference/actionscript/3/flash/net/NetStream.html#seek() 396 | [MJPEG/HTTP]: http://en.wikipedia.org/wiki/Motion_JPEG#M-JPEG_over_HTTP 397 | 398 | ## License 399 | 400 | This project is licensed under the [BSD 3-Clause License](https://opensource.org/licenses/BSD-3-Clause). 401 | See [LICENSE](https://github.com/AxisCommunications/locomote-video-player/blob/master/LICENSE) file. 402 | -------------------------------------------------------------------------------- /src/Player.as: -------------------------------------------------------------------------------- 1 | package { 2 | import com.axis.audioclient.AxisTransmit; 3 | import com.axis.ClientEvent; 4 | import com.axis.ErrorManager; 5 | import com.axis.http.url; 6 | import com.axis.httpclient.HTTPClient; 7 | import com.axis.IClient; 8 | import com.axis.Logger; 9 | import com.axis.mjpegclient.MJPEGClient; 10 | import com.axis.rtmpclient.RTMPClient; 11 | import com.axis.rtspclient.IRTSPHandle; 12 | import com.axis.rtspclient.RTSPClient; 13 | import com.axis.rtspclient.RTSPoverHTTPHandle; 14 | import com.axis.rtspclient.RTSPoverHTTPAPHandle; 15 | import com.axis.rtspclient.RTSPoverTCPHandle; 16 | 17 | import flash.display.LoaderInfo; 18 | import flash.display.Sprite; 19 | import flash.display.Stage; 20 | import flash.display.StageAlign; 21 | import flash.display.StageDisplayState; 22 | import flash.display.StageScaleMode; 23 | import flash.display.DisplayObject; 24 | import flash.display.InteractiveObject; 25 | import flash.events.Event; 26 | import flash.events.MouseEvent; 27 | import flash.external.ExternalInterface; 28 | import flash.media.Microphone; 29 | import flash.media.SoundMixer; 30 | import flash.media.SoundTransform; 31 | import flash.media.Video; 32 | import flash.net.NetStream; 33 | import flash.system.Security; 34 | import mx.utils.StringUtil; 35 | 36 | [SWF(frameRate="60")] 37 | [SWF(backgroundColor="#efefef")] 38 | 39 | public class Player extends Sprite { 40 | [Embed(source = "../VERSION", mimeType = "application/octet-stream")] private var Version:Class; 41 | 42 | public static var locomoteID:String = null; 43 | 44 | private static const EVENT_STREAM_STARTED:String = "streamStarted"; 45 | private static const EVENT_STREAM_PAUSED:String = "streamPaused"; 46 | private static const EVENT_STREAM_STOPPED:String = "streamStopped"; 47 | private static const EVENT_FULLSCREEN_ENTERED:String = "fullscreenEntered"; 48 | private static const EVENT_FULLSCREEN_EXITED:String = "fullscreenExited"; 49 | private static const EVENT_FRAME_READY:String = "frameReady"; 50 | 51 | public static var config:Object = { 52 | 'buffer': 3, 53 | 'keepAlive': 0, 54 | 'connectionTimeout': 10, 55 | 'scaleUp': false, 56 | 'allowFullscreen': true, 57 | 'debugLogger': false, 58 | 'frameByFrame': false 59 | }; 60 | 61 | private var audioTransmit:AxisTransmit = new AxisTransmit(); 62 | private var meta:Object = {}; 63 | private var client:IClient; 64 | private var urlParsed:Object; 65 | private var savedSpeakerVolume:Number; 66 | private var fullscreenAllowed:Boolean = true; 67 | private var currentState:String = "stopped"; 68 | private var streamHasAudio:Boolean = false; 69 | private var streamHasVideo:Boolean = false; 70 | private var newPlaylistItem:Boolean = false; 71 | private var startOptions:Object = null; 72 | 73 | public function Player() { 74 | var self:Player = this; 75 | 76 | Security.allowDomain("*"); 77 | Security.allowInsecureDomain("*"); 78 | 79 | trace('Loaded Locomote, version ' + StringUtil.trim(new Version().toString())); 80 | 81 | if (ExternalInterface.available) { 82 | setupAPICallbacks(); 83 | } else { 84 | trace("External interface is not available for this container."); 85 | } 86 | 87 | /* Set default speaker volume */ 88 | this.speakerVolume(50); 89 | 90 | /* Stage setup */ 91 | this.stage.align = StageAlign.TOP_LEFT; 92 | this.stage.scaleMode = StageScaleMode.NO_SCALE; 93 | addEventListener(Event.ADDED_TO_STAGE, onStageAdded); 94 | 95 | /* Fullscreen support setup */ 96 | this.stage.doubleClickEnabled = true; 97 | this.stage.addEventListener(MouseEvent.DOUBLE_CLICK, fullscreen); 98 | this.stage.addEventListener(Event.FULLSCREEN, function(event:Event):void { 99 | (StageDisplayState.NORMAL === stage.displayState) ? callAPI(EVENT_FULLSCREEN_EXITED) : callAPI(EVENT_FULLSCREEN_ENTERED); 100 | }); 101 | this.stage.addEventListener(Event.RESIZE, function(event:Event):void { 102 | videoResize(); 103 | }); 104 | 105 | this.setConfig(Player.config); 106 | } 107 | 108 | /** 109 | * Registers the appropriate API functions with the container, so that 110 | * they can be called, and triggers the apiReady event 111 | * which tells the container that the Player is ready to receive API calls. 112 | */ 113 | public function setupAPICallbacks():void { 114 | ExternalInterface.marshallExceptions = true; 115 | 116 | /* Media player API */ 117 | ExternalInterface.addCallback("play", play); 118 | ExternalInterface.addCallback("pause", pause); 119 | ExternalInterface.addCallback("resume", resume); 120 | ExternalInterface.addCallback("stop", stop); 121 | ExternalInterface.addCallback("seek", seek); 122 | ExternalInterface.addCallback("playFrames", playFrames); 123 | ExternalInterface.addCallback("streamStatus", streamStatus); 124 | ExternalInterface.addCallback("playerStatus", playerStatus); 125 | ExternalInterface.addCallback("speakerVolume", speakerVolume); 126 | ExternalInterface.addCallback("muteSpeaker", muteSpeaker); 127 | ExternalInterface.addCallback("unmuteSpeaker", unmuteSpeaker); 128 | ExternalInterface.addCallback("microphoneVolume", microphoneVolume); 129 | ExternalInterface.addCallback("muteMicrophone", muteMicrophone); 130 | ExternalInterface.addCallback("unmuteMicrophone", unmuteMicrophone); 131 | ExternalInterface.addCallback("setConfig", setConfig); 132 | ExternalInterface.addCallback("loadPolicyFile", loadPolicyFile); 133 | 134 | /* Audio Transmission API */ 135 | ExternalInterface.addCallback("startAudioTransmit", startAudioTransmit); 136 | ExternalInterface.addCallback("stopAudioTransmit", stopAudioTransmit); 137 | } 138 | 139 | public function fullscreen(event:MouseEvent):void { 140 | if (config.allowFullscreen) { 141 | this.stage.displayState = (StageDisplayState.NORMAL === stage.displayState) ? 142 | StageDisplayState.FULL_SCREEN : StageDisplayState.NORMAL; 143 | } 144 | } 145 | 146 | public function videoResize():void { 147 | if (!this.client) { 148 | return; 149 | } 150 | 151 | var stagewidth:uint = (StageDisplayState.NORMAL === stage.displayState) ? 152 | stage.stageWidth : stage.fullScreenWidth; 153 | var stageheight:uint = (StageDisplayState.NORMAL === stage.displayState) ? 154 | stage.stageHeight : stage.fullScreenHeight; 155 | 156 | var video:DisplayObject = this.client.getDisplayObject(); 157 | 158 | var scale:Number = ((stagewidth / meta.width) > (stageheight / meta.height)) ? 159 | (stageheight / meta.height) : (stagewidth / meta.width); 160 | 161 | video.width = meta.width; 162 | video.height = meta.height; 163 | if ((scale < 1.0) || (scale > 1.0 && true === config.scaleUp)) { 164 | Logger.log('scaling video, scale:' + scale.toFixed(2) + ' (aspect ratio: ' + (video.width / video.height).toFixed(2) + ')'); 165 | video.width = meta.width * scale; 166 | video.height = meta.height * scale; 167 | } 168 | 169 | video.x = (stagewidth - video.width) / 2; 170 | video.y = (stageheight - video.height) / 2; 171 | } 172 | 173 | public function setConfig(iconfig:Object):void { 174 | if (iconfig.buffer !== undefined) { 175 | if (this.client) { 176 | if (false === this.client.setBuffer(config.buffer)) { 177 | ErrorManager.dispatchError(830); 178 | } else { 179 | config.buffer = iconfig.buffer; 180 | } 181 | } else { 182 | config.buffer = iconfig.buffer; 183 | } 184 | } 185 | 186 | if (iconfig.keepAlive !== undefined) { 187 | if (this.client && !this.client.setKeepAlive(config.keepAlive)) { 188 | ErrorManager.dispatchError(834); 189 | } else { 190 | config.keepAlive = iconfig.keepAlive; 191 | } 192 | } 193 | 194 | if (iconfig.frameByFrame !== undefined) { 195 | if (this.client) { 196 | if (false === this.client.setFrameByFrame(iconfig.frameByFrame)) { 197 | ErrorManager.dispatchError(832); 198 | } else { 199 | config.frameByFrame = iconfig.frameByFrame; 200 | } 201 | } else { 202 | config.frameByFrame = iconfig.frameByFrame; 203 | } 204 | } 205 | 206 | if (iconfig.scaleUp !== undefined) { 207 | var scaleUpChanged:Boolean = (config.scaleUp !== iconfig.scaleUp); 208 | config.scaleUp = iconfig.scaleUp; 209 | if (scaleUpChanged && this.client) 210 | this.videoResize(); 211 | } 212 | 213 | if (iconfig.allowFullscreen !== undefined) { 214 | config.allowFullscreen = iconfig.allowFullscreen; 215 | 216 | if (!config.allowFullscreen) 217 | this.stage.displayState = StageDisplayState.NORMAL; 218 | } 219 | 220 | if (iconfig.debugLogger !== undefined) { 221 | config.debugLogger = iconfig.debugLogger; 222 | } 223 | 224 | if (iconfig.connectionTimeout !== undefined) { 225 | config.connectionTimeout = iconfig.connectionTimeout; 226 | } 227 | } 228 | 229 | public function loadPolicyFile(url:String):String { 230 | Security.loadPolicyFile(url); 231 | return "ok"; 232 | } 233 | 234 | public function play(param:* = null, options:Object = null):void { 235 | this.streamHasAudio = false; 236 | this.streamHasVideo = false; 237 | 238 | if (param is String) { 239 | urlParsed = url.parse(String(param)); 240 | } else { 241 | urlParsed = url.parse(param.url); 242 | urlParsed.connect = param.url; 243 | urlParsed.streamName = param.streamName; 244 | } 245 | 246 | this.newPlaylistItem = true; 247 | this.startOptions = options; 248 | 249 | if (client) { 250 | /* Stop the client, and 'onStopped' will start the new stream. */ 251 | client.stop(); 252 | return; 253 | } 254 | 255 | start(); 256 | } 257 | 258 | private function start():void { 259 | switch (urlParsed.protocol) { 260 | case 'rtsph': 261 | /* RTSP over HTTP */ 262 | client = new RTSPClient(urlParsed, new RTSPoverHTTPHandle(this.startOptions && this.startOptions.httpUrl ? url.parse(this.startOptions.httpUrl) : urlParsed, false)); 263 | break; 264 | 265 | case 'rtsphs': 266 | /* RTSP over HTTPS */ 267 | client = new RTSPClient(urlParsed, new RTSPoverHTTPHandle(this.startOptions && this.startOptions.httpUrl ? url.parse(this.startOptions.httpUrl) : urlParsed, true)); 268 | break; 269 | 270 | case 'rtsphap': 271 | /* RTSP over HTTP via Axis Proxy */ 272 | client = new RTSPClient(urlParsed, new RTSPoverHTTPAPHandle(urlParsed, true)); 273 | break; 274 | 275 | case 'rtsp': 276 | /* RTSP over TCP */ 277 | client = new RTSPClient(urlParsed, new RTSPoverTCPHandle(urlParsed)); 278 | break; 279 | 280 | case 'http': 281 | case 'https': 282 | /* Progressive download over HTTP */ 283 | client = new HTTPClient(urlParsed); 284 | break; 285 | 286 | case 'httpm': 287 | /* Progressive mjpg download over HTTP (x-mixed-replace) */ 288 | client = new MJPEGClient(urlParsed); 289 | break; 290 | 291 | case 'rtmp': 292 | case 'rtmps': 293 | case 'rtmpt': 294 | /* RTMP */ 295 | client = new RTMPClient(urlParsed); 296 | break; 297 | 298 | default: 299 | ErrorManager.dispatchError(811, [urlParsed.protocol]) 300 | return; 301 | } 302 | 303 | addChild(this.client.getDisplayObject()); 304 | 305 | client.addEventListener(ClientEvent.STOPPED, onStopped); 306 | client.addEventListener(ClientEvent.START_PLAY, onStartPlay); 307 | client.addEventListener(ClientEvent.PAUSED, onPaused); 308 | client.addEventListener(ClientEvent.META, onMeta); 309 | client.addEventListener(ClientEvent.FRAME, onFrame); 310 | client.start(this.startOptions); 311 | this.newPlaylistItem = false; 312 | } 313 | 314 | public function seek(position:String):void { 315 | if (!client || !client.seek(Number(position))) { 316 | ErrorManager.dispatchError(828); 317 | } 318 | } 319 | 320 | public function playFrames(timestamp:Number):void { 321 | client && client.playFrames(timestamp); 322 | } 323 | 324 | public function pause():void { 325 | try { 326 | client.pause() 327 | } catch (err:Error) { 328 | ErrorManager.dispatchError(808); 329 | } 330 | } 331 | 332 | public function resume():void { 333 | if (!client || !client.resume()) { 334 | ErrorManager.dispatchError(809); 335 | } 336 | } 337 | 338 | public function stop():void { 339 | if (!client || !client.stop()) { 340 | ErrorManager.dispatchError(810); 341 | return; 342 | } 343 | 344 | this.currentState = "stopped"; 345 | this.streamHasAudio = false; 346 | this.streamHasVideo = false; 347 | } 348 | 349 | public function onMeta(event:ClientEvent):void { 350 | this.meta = event.data; 351 | this.videoResize(); 352 | } 353 | 354 | public function streamStatus():Object { 355 | var status:Object = { 356 | 'fps': (this.client) ? this.client.currentFPS() : null, 357 | 'resolution': (this.client) ? { width: meta.width, height: meta.height } : null, 358 | 'playbackSpeed': (this.client) ? 1.0 : null, 359 | 'protocol': (this.urlParsed) ? this.urlParsed.protocol : null, 360 | 'state': this.currentState, 361 | 'streamURL': (this.urlParsed) ? this.urlParsed.full : null, 362 | 'duration': meta.duration ? meta.duration : null, 363 | 'currentTime': (this.client) ? this.client.getCurrentTime() : -1, 364 | 'bufferedTime': (this.client) ? this.client.bufferedTime() : -1 365 | }; 366 | 367 | return status; 368 | } 369 | 370 | public function playerStatus():Object { 371 | var mic:Microphone = Microphone.getMicrophone(); 372 | 373 | var status:Object = { 374 | 'version': StringUtil.trim(new Version().toString()), 375 | 'microphoneVolume': audioTransmit.microphoneVolume, 376 | 'speakerVolume': this.savedSpeakerVolume, 377 | 'microphoneMuted': (mic.gain === 0), 378 | 'speakerMuted': (flash.media.SoundMixer.soundTransform.volume === 0), 379 | 'fullscreen': (StageDisplayState.FULL_SCREEN === stage.displayState), 380 | 'buffer': (client === null) ? 0 : Player.config.buffer 381 | }; 382 | 383 | return status; 384 | } 385 | 386 | public function speakerVolume(volume:Number):void { 387 | this.savedSpeakerVolume = volume; 388 | var transform:SoundTransform = new SoundTransform(volume / 100.0); 389 | flash.media.SoundMixer.soundTransform = transform; 390 | } 391 | 392 | public function muteSpeaker():void { 393 | var transform:SoundTransform = new SoundTransform(0); 394 | flash.media.SoundMixer.soundTransform = transform; 395 | } 396 | 397 | public function unmuteSpeaker():void { 398 | if (flash.media.SoundMixer.soundTransform.volume !== 0) 399 | return; 400 | 401 | var transform:SoundTransform = new SoundTransform(this.savedSpeakerVolume / 100.0); 402 | flash.media.SoundMixer.soundTransform = transform; 403 | } 404 | 405 | public function microphoneVolume(volume:Number):void { 406 | audioTransmit.microphoneVolume = volume; 407 | } 408 | 409 | public function muteMicrophone():void { 410 | audioTransmit.muteMicrophone(); 411 | } 412 | 413 | public function unmuteMicrophone():void { 414 | audioTransmit.unmuteMicrophone(); 415 | } 416 | 417 | public function startAudioTransmit(url:String = null, type:String = 'axis'):void { 418 | if (type === 'axis') { 419 | audioTransmit.start(url); 420 | } else { 421 | ErrorManager.dispatchError(812); 422 | } 423 | } 424 | 425 | public function stopAudioTransmit():void { 426 | audioTransmit.stop(); 427 | } 428 | 429 | public function allowFullscreen(state:Boolean):void { 430 | this.fullscreenAllowed = state; 431 | 432 | if (!state) 433 | this.stage.displayState = StageDisplayState.NORMAL; 434 | } 435 | 436 | public static function isUserAgentIE():Boolean { 437 | var useragent:String = '' 438 | try { 439 | useragent = ExternalInterface.call("function(){return navigator.userAgent;}"); 440 | } catch (e:Error) { 441 | Logger.log('Unable to get user Agent'); 442 | } 443 | return ((useragent.indexOf("Trident") != -1 || 444 | useragent.indexOf("Edge") != -1)); 445 | } 446 | 447 | private function onStageAdded(e:Event):void { 448 | Player.locomoteID = LoaderInfo(this.root.loaderInfo).parameters.locomoteID.toString(); 449 | ExternalInterface.call("LocomoteMap['" + Player.locomoteID + "'].__swfReady"); 450 | } 451 | 452 | private function onStartPlay(event:ClientEvent):void { 453 | this.currentState = "playing"; 454 | this.callAPI(EVENT_STREAM_STARTED); 455 | } 456 | 457 | private function onPaused(event:ClientEvent):void { 458 | this.currentState = "paused"; 459 | this.callAPI(EVENT_STREAM_PAUSED, event.data); 460 | } 461 | 462 | private function onStopped(event:ClientEvent):void { 463 | this.removeChild(this.client.getDisplayObject()); 464 | this.client.removeEventListener(ClientEvent.STOPPED, onStopped); 465 | this.client.removeEventListener(ClientEvent.START_PLAY, onStartPlay); 466 | this.client.removeEventListener(ClientEvent.PAUSED, onPaused); 467 | this.client.removeEventListener(ClientEvent.META, onMeta); 468 | this.client.removeEventListener(ClientEvent.FRAME, onFrame); 469 | this.client = null; 470 | this.callAPI(EVENT_STREAM_STOPPED); 471 | 472 | /* If a new `play` has been queued, fire it */ 473 | if (this.newPlaylistItem) { 474 | start(); 475 | } 476 | } 477 | 478 | private function onFrame(event:ClientEvent):void { 479 | this.callAPI(EVENT_FRAME_READY, { timestamp: event.data }); 480 | } 481 | 482 | private function callAPI(eventName:String, data:Object = null):void { 483 | var functionName:String = "LocomoteMap['" + Player.locomoteID + "'].__playerEvent"; 484 | if (data) { 485 | ExternalInterface.call(functionName, eventName, data); 486 | } else { 487 | ExternalInterface.call(functionName, eventName); 488 | } 489 | } 490 | } 491 | } 492 | -------------------------------------------------------------------------------- /src/com/axis/rtspclient/FLVMux.as: -------------------------------------------------------------------------------- 1 | package com.axis.rtspclient { 2 | import com.axis.ClientEvent; 3 | import com.axis.ErrorManager; 4 | import com.axis.Logger; 5 | import com.axis.rtspclient.ByteArrayUtils; 6 | import com.axis.rtspclient.RTP; 7 | import com.axis.rtspclient.FLVTag; 8 | 9 | import flash.events.Event; 10 | import flash.events.EventDispatcher; 11 | import flash.net.FileReference; 12 | import flash.net.NetStream; 13 | import flash.utils.ByteArray; 14 | 15 | import mx.utils.Base64Decoder; 16 | 17 | public class FLVMux extends EventDispatcher { 18 | private const EMPTY_BUF:ByteArray = new ByteArray(); 19 | 20 | private var sdp:SDP; 21 | private var container:ByteArray = new ByteArray(); 22 | private var loggedBytes:ByteArray = new ByteArray(); 23 | private var lastTimestamp:Number = -1; 24 | private var firstTimestamp:Number = -1; 25 | 26 | private var sps:ByteArray = EMPTY_BUF; 27 | private var pps:ByteArray = EMPTY_BUF; 28 | 29 | public function FLVMux(sdp:SDP) { 30 | container.writeByte(0x46); // 'F' 31 | container.writeByte(0x4C); // 'L' 32 | container.writeByte(0x56); // 'V' 33 | container.writeByte(0x01); // Version 1 34 | container.writeByte( 35 | (sdp.getMediaBlock('audio') ? 0x01 : 0x00) << 2 | 36 | (sdp.getMediaBlock('video') ? 0x01 : 0x00) << 0 37 | ); 38 | container.writeUnsignedInt(0x09) // Reserved: usually is 0x09 39 | container.writeUnsignedInt(0x0) // Previous tag size: shall be 0 40 | 41 | this.sdp = sdp; 42 | 43 | if (sdp.getMediaBlock('video') && sdp.getMediaBlock('video').hasOwnProperty('fmtp')) { 44 | /* Initial parameters must be taken from SDP file. Additional may be received as NAL */ 45 | var sets:Array = sdp.getMediaBlock('video').fmtp['sprop-parameter-sets'].split(','); 46 | var sps:Base64Decoder = new Base64Decoder(); 47 | var pps:Base64Decoder = new Base64Decoder(); 48 | sps.decode(sets[0]); 49 | pps.decode(sets[1]); 50 | this.sps = sps.toByteArray(); 51 | this.pps = pps.toByteArray(); 52 | createVideoSpecificConfigTag(); 53 | } 54 | 55 | if (sdp.getMediaBlock('audio')) { 56 | createAudioSpecificConfigTag(sdp.getMediaBlock('audio')); 57 | } 58 | } 59 | 60 | private function createVideoSpecificConfigTag():void { 61 | if (this.sps.bytesAvailable != 0 && this.pps.bytesAvailable != 0) { 62 | var spsbit:BitArray = new BitArray(sps); 63 | var params:Object = parseSPS(spsbit); 64 | createDecoderConfigRecordTag(sps, pps, params); 65 | createMetaDataTag(params); 66 | } 67 | } 68 | 69 | private function writeECMAArray(contents:Object):uint { 70 | var size:uint = 0; 71 | var count:uint = 0; 72 | 73 | for (var s:String in contents) count++; 74 | 75 | container.writeByte(0x08); // ECMA Array Type 76 | container.writeUnsignedInt(count); // (Approximate) number of elements in ECMA array 77 | size += 1 + 4; 78 | 79 | for (var key:String in contents) { 80 | container.writeShort(key.length); // Length of key 81 | container.writeUTFBytes(key); // The key itself 82 | size += 2 + key.length; 83 | switch(contents[key]) { 84 | case contents[key] as Number: 85 | size += writeDouble(contents[key]); 86 | break; 87 | 88 | case contents[key] as String: 89 | size += writeString(contents[key]); 90 | break; 91 | 92 | default: 93 | Logger.log("Unknown type in ECMA array:" + typeof contents[key]); 94 | 95 | break; 96 | } 97 | } 98 | 99 | /* ECMA Array End */ 100 | container.writeByte(0x00); 101 | container.writeByte(0x00); 102 | container.writeByte(0x09); 103 | size += 3; 104 | 105 | return size; 106 | } 107 | 108 | private function writeDouble(contents:Number):uint { 109 | container.writeByte(0x00); // Number type marker 110 | container.writeDouble(contents); 111 | return 1 + 8; 112 | } 113 | 114 | private function writeString(contents:String):uint { 115 | container.writeByte(0x02); // String type marker 116 | container.writeShort(contents.length); // Length of string 117 | container.writeUTFBytes(contents); // String 118 | return 1 + 2 + contents.length; 119 | } 120 | 121 | private function parseSPS(sps:BitArray):Object { 122 | var nalhdr:uint = sps.readBits(8); 123 | 124 | var profile:uint = sps.readBits(8); 125 | Logger.log('FLVMux: sps profile =', profile); 126 | 127 | var constraints:uint = sps.readBits(8); 128 | Logger.log('FLVMUX: sps constraints =', constraints); 129 | 130 | var level:uint = sps.readBits(8); 131 | Logger.log('FLVMux: sps level =', level); 132 | 133 | var seq_parameter_set_id:uint = sps.readUnsignedExpGolomb(); 134 | if (-1 !== [100, 110, 122, 244, 44, 83, 86, 118, 128, 138].indexOf(profile)) { 135 | /* Parse chroma/luma parameters */ 136 | var chroma_format_idc:uint = sps.readUnsignedExpGolomb(); 137 | if (3 === chroma_format_idc) { 138 | var separate_colour_plane_flag:uint = sps.readBits(1); 139 | } 140 | 141 | var bit_depth_luma_minus8:uint = sps.readUnsignedExpGolomb(); 142 | var bit_depth_chroma_minus8:uint = sps.readUnsignedExpGolomb(); 143 | var qpprime_y_zero_transform_bypass_flag:uint = sps.readBits(1); 144 | var seq_scaling_matrix_present_flag:uint = sps.readBits(1); 145 | 146 | if (seq_scaling_matrix_present_flag) { 147 | var i:uint = 0; 148 | var loopCount: uint = (3 === chroma_format_idc) ? 12 : 8; 149 | for (i = 0; i < loopCount; i++) { 150 | var seq_scaling_list_present_flag:uint = sps.readBits(1); 151 | if (seq_scaling_list_present_flag) { 152 | var sizeOfScalingList:uint = (i < 6) ? 16 : 64; 153 | var lastScale:uint = 8; 154 | var nextScale:uint = 8; 155 | var j:uint = 0; 156 | for (j = 0; j < sizeOfScalingList; j++) { 157 | if (nextScale != 0) { 158 | var delta_scale:uint = sps.readSignedExpGolomb(); 159 | nextScale = (lastScale + delta_scale + 256) % 256; 160 | } 161 | lastScale = (nextScale == 0) ? lastScale : nextScale; 162 | } 163 | } 164 | } 165 | } 166 | } 167 | 168 | var log2_max_frame_num_minus4:uint = sps.readUnsignedExpGolomb(); 169 | var pic_order_cnt_type:uint = sps.readUnsignedExpGolomb(); 170 | if (0 == pic_order_cnt_type) { 171 | var log2_max_pic_order_cnt_lsb_minus4:uint = sps.readUnsignedExpGolomb(); 172 | } else if (1 == pic_order_cnt_type) { 173 | ErrorManager.dispatchError(823, null, true); 174 | } 175 | 176 | var max_num_ref_frames:uint = sps.readUnsignedExpGolomb(); 177 | var gaps_in_frame_num_value_allowed_flag:uint = sps.readBits(1); 178 | var pic_width_in_mbs_minus1:uint = sps.readUnsignedExpGolomb(); 179 | var pic_height_in_map_units_minus1:uint = sps.readUnsignedExpGolomb(); 180 | var pic_frame_mbs_only_flag:uint = sps.readBits(1); 181 | var direct_8x8_inference_flag:uint = sps.readBits(1); 182 | var frame_cropping_flag:uint = sps.readBits(1); 183 | var frame_crop_left_offset:uint = frame_cropping_flag ? sps.readUnsignedExpGolomb() : 0; 184 | var frame_crop_right_offset:uint = frame_cropping_flag ? sps.readUnsignedExpGolomb() : 0; 185 | var frame_crop_top_offset:uint = frame_cropping_flag ? sps.readUnsignedExpGolomb() : 0; 186 | var frame_crop_bottom_offset:uint = frame_cropping_flag ? sps.readUnsignedExpGolomb() : 0; 187 | 188 | var w:uint = (pic_width_in_mbs_minus1 + 1) * 16 - 189 | (frame_crop_left_offset * 2) - (frame_crop_right_offset * 2); 190 | var h:uint = (2 - pic_frame_mbs_only_flag) * (pic_height_in_map_units_minus1 + 1) * 16 - 191 | (frame_crop_top_offset * 2) - (frame_crop_bottom_offset * 2) 192 | return { 193 | 'profile' : profile, 194 | 'level' : level / 10.0, 195 | 'width' : w, 196 | 'height' : h 197 | }; 198 | } 199 | 200 | public function createMetaDataTag(params:Object):void { 201 | var size:uint = 0; 202 | 203 | /* FLV Tag */ 204 | var sizePosition:uint = container.position + 1; // 'Size' is the 24 last byte of the next uint 205 | container.writeUnsignedInt(0x00000012 << 24 | (size & 0x00FFFFFF)); // Type << 24 | size & 0x00FFFFFF 206 | container.writeUnsignedInt(0x00000000); // Timestamp & TimestampExtended 207 | container.writeByte(0x00); // StreamID - always 0 208 | container.writeByte(0x00); // StreamID - always 0 209 | container.writeByte(0x00); // StreamID - always 0 210 | size += 4 + 4 + 3; 211 | 212 | /* Method call */ 213 | size += writeString("onMetaData"); 214 | 215 | /* Arguments */ 216 | size += writeECMAArray({ 217 | videocodecid : 7.0, /* Only support AVC (H.264) */ 218 | width : params.width, 219 | height : params.height, 220 | avcprofile : params.profile, 221 | avclevel : params.level, 222 | metadatacreator : "Locomote FLV Muxer", 223 | creationdate : new Date().toString() 224 | }); 225 | 226 | container.writeUnsignedInt(size); // Previous tag size 227 | 228 | /* Rewind and set the data size in tag header to actual size */ 229 | var dataSize:uint = size - 11; 230 | container[sizePosition + 0] = dataSize & 0x00FF0000; 231 | container[sizePosition + 1] = dataSize & 0x0000FF00; 232 | container[sizePosition + 2] = dataSize & 0x000000FF; 233 | 234 | } 235 | 236 | public function createDecoderConfigRecordTag(sps:ByteArray, pps:ByteArray, params:Object):void { 237 | var start:uint = container.position; 238 | 239 | /* FLV Tag */ 240 | var sizePosition:uint = container.position + 1; // 'Size' is the 24 last byte of the next uint 241 | container.writeUnsignedInt(0x00000009 << 24 | (0x000000 & 0x00FFFFFF)); // Type << 24 | size & 0x00FFFFFF 242 | container.writeUnsignedInt(0x00000000); // Timestamp & TimestampExtended 243 | container.writeByte(0x00); // StreamID - always 0 244 | container.writeByte(0x00); // StreamID - always 0 245 | container.writeByte(0x00); // StreamID - always 0 246 | 247 | /* Video Tag Header */ 248 | container.writeByte(0x01 << 4 | 0x07); // Keyframe << 4 | CodecID 249 | container.writeUnsignedInt(0x00 << 24 | 0x00000000); // AVC NALU << 24 | CompositionTime 250 | 251 | var profilelevelid:uint = parseInt(params.profile, 16); 252 | writeDecoderConfigurationRecord(profilelevelid); 253 | writeParameterSets(sps, pps); 254 | this.sps = EMPTY_BUF; 255 | this.pps = EMPTY_BUF; 256 | 257 | var size:uint = container.position - start; 258 | 259 | /* Rewind and set the data size in tag header to actual size */ 260 | var dataSize:uint = size - 11; 261 | container[sizePosition + 0] = (dataSize >> 16 & 0xFF); 262 | container[sizePosition + 1] = (dataSize >> 8 & 0xFF); 263 | container[sizePosition + 2] = (dataSize >> 0 & 0xFF); 264 | 265 | /* End of tag */ 266 | container.writeUnsignedInt(size); 267 | } 268 | 269 | public function writeDecoderConfigurationRecord(profilelevelid:uint):void { 270 | container.writeByte(0x01); // Version 271 | container.writeByte((profilelevelid & 0x00FF0000) >> 16); // AVC Profile, Baseline 272 | container.writeByte((profilelevelid & 0x0000FF00) >> 8); // Profile compatibility 273 | container.writeByte((profilelevelid & 0x000000FF) >> 0); // Level indication 274 | container.writeByte(0xFF); // 111111xx (xx=lengthSizeMinusOne) 275 | } 276 | 277 | public function writeParameterSets(sps:ByteArray, pps:ByteArray):void { 278 | if (sps.bytesAvailable > 0) { 279 | /* There is one sps available */ 280 | container.writeByte(0xE1); // 111xxxxx (xxxxx=numSequenceParameters), only support 1 281 | container.writeShort(sps.bytesAvailable); // Sequence parameter set 1 length 282 | container.writeBytes(sps, sps.position); // Actual parameters 283 | } else { 284 | /* No sps here */ 285 | container.writeByte(0xE0); // 111xxxxx (xxxxx=numOfSequenceParameters), 0 sps here 286 | } 287 | 288 | if (pps.bytesAvailable > 0) { 289 | container.writeByte(0x01); // Num picture parameters, only support 1 290 | container.writeShort(pps.bytesAvailable); // Picture parameter length 291 | container.writeBytes(pps, pps.position); // Actual parameters 292 | } else { 293 | /* No pps here */ 294 | container.writeByte(0x00); // numOfPictureParameterSets 295 | } 296 | } 297 | 298 | private function getAudioParameters(name:String):Object { 299 | var sdpMedia:Object = this.sdp.getMediaBlock('audio'); 300 | switch (name.toLowerCase()) { 301 | case 'mpeg4-generic': 302 | return { 303 | format: 0xA, /* AAC */ 304 | sampling: 0x3, /* Should alway be 0x3. Actual rate is determined by AAC header. */ 305 | depth: 0x1, /* 16 bits per sample */ 306 | type: 0x1, /* Stereo */ 307 | duration: 1024 * 1000 / sdpMedia.rtpmap[sdpMedia.fmt[0]].clock /* An AAC frame contains 1024 samples */ 308 | }; 309 | case 'pcma': 310 | return { 311 | format: 0x7, /* Logarithmic G.711 A-law */ 312 | sampling: 0x0, /* Doesn't matter. Rate is fixed at 8 kHz when format = 0x7 */ 313 | depth: 0x1, /* 16 bits per sample, but why? */ 314 | type: 0x0, /* Mono */ 315 | duration: 0 /* not implemented */ 316 | }; 317 | default: 318 | return false; 319 | } 320 | } 321 | 322 | public function createAudioSpecificConfigTag(config:Object):void { 323 | var start:uint = container.position; 324 | 325 | /* FLV Tag */ 326 | var sizePosition:uint = container.position + 1; // 'Size' is the 24 last byte of the next uint 327 | container.writeUnsignedInt(0x00000008 << 24 | (0x000000 & 0x00FFFFFF)); // Type << 24 | size & 0x00FFFFFF 328 | container.writeUnsignedInt(0x00000000); // Timestamp & TimestampExtended 329 | container.writeByte(0x00); // StreamID - always 0 330 | container.writeByte(0x00); // StreamID - always 0 331 | container.writeByte(0x00); // StreamID - always 0 332 | 333 | var audioParams:Object; 334 | for (var pt:Object in config.rtpmap) { 335 | audioParams = getAudioParameters(config.rtpmap[pt].name); 336 | if (false !== audioParams) { 337 | break; 338 | } 339 | /* Didn't get any params on first type, and multiple types are not supported. */ 340 | ErrorManager.dispatchError(831, [ config.rtpmap[pt].name ], true); 341 | } 342 | 343 | /* Audio Tag Header */ 344 | container.writeByte(audioParams.format << 4 | audioParams.sampling << 2 | audioParams.depth << 1 | audioParams.type << 0); 345 | 346 | if (0xA === audioParams.format) { 347 | /* A little more setup required if this is AAC */ 348 | container.writeByte(0x0); // AAC Sequence Header 349 | container.writeBytes(ByteArrayUtils.createFromHexstring(config.fmtp['config'])); 350 | } 351 | 352 | var size:uint = container.position - start; 353 | 354 | /* Rewind and set the data size in tag header to actual size */ 355 | var dataSize:uint = size - 11; 356 | container[sizePosition + 0] = (dataSize >> 16 & 0xFF); 357 | container[sizePosition + 1] = (dataSize >> 8 & 0xFF); 358 | container[sizePosition + 2] = (dataSize >> 0 & 0xFF); 359 | 360 | /* End of tag */ 361 | container.writeUnsignedInt(size); 362 | } 363 | 364 | private function createVideoTag(nalu:NALU):void { 365 | var start:uint = container.position; 366 | var ts:uint = nalu.timestamp; 367 | // Video and audio packets may arrive out of order. In that case set new 368 | // first timestamp. 369 | if (this.firstTimestamp === -1 || ts < this.firstTimestamp) { 370 | this.firstTimestamp = ts; 371 | } 372 | ts -= firstTimestamp; 373 | 374 | /* FLV Tag */ 375 | var sizePosition:uint = container.position + 1; // 'Size' is the 24 last byte of the next uint 376 | container.writeUnsignedInt(0x09 << 24 | (size & 0x00FFFFFF)); // Type << 24 | size & 0x00FFFFFF 377 | container.writeUnsignedInt(((ts >>> 24) & 0xFF) | ((ts << 8) & 0xFFFFFF00)); 378 | container.writeByte(0x00); 379 | container.writeByte(0x00); 380 | container.writeByte(0x00); 381 | 382 | /* Video Tag Header */ 383 | container.writeByte((nalu.isIDR() ? 1 : 2) << 4 | 0x07); // Keyframe << 4 | CodecID 384 | container.writeUnsignedInt(0x01 << 24 | (0x0 & 0x00FFFFFF)); // AVC NALU << 24 | CompositionTime & 0x00FFFFFF 385 | 386 | /* Video Data */ 387 | nalu.writeStream(container); 388 | 389 | var size:uint = container.position - start; 390 | 391 | /* Rewind and set the data size in tag header to actual size */ 392 | var dataSize:uint = size - 11; 393 | 394 | container[sizePosition + 0] = (dataSize >> 16 & 0xFF); 395 | container[sizePosition + 1] = (dataSize >> 8 & 0xFF); 396 | container[sizePosition + 2] = (dataSize >> 0 & 0xFF); 397 | 398 | /* Previous Tag Size */ 399 | container.writeUnsignedInt(size + 11); 400 | this.lastTimestamp = ts; 401 | 402 | createFLVTag(nalu.timestamp, 0, false); 403 | } 404 | 405 | public function createAudioTag(name:String, frame:*):void { 406 | var start:uint = container.position; 407 | var ts:uint = frame.timestamp; 408 | // Video and audio packets may arrive out of order. In that case set new 409 | // first timestamp. 410 | if (this.firstTimestamp === -1 || ts < this.firstTimestamp) { 411 | this.firstTimestamp = ts; 412 | } 413 | ts -= firstTimestamp; 414 | 415 | /* FLV Tag */ 416 | var sizePosition:uint = container.position + 1; // 'Size' is the 24 last byte of the next uint 417 | container.writeUnsignedInt(0x00000008 << 24 | (0x000000 & 0x00FFFFFF)); // Type << 24 | size & 0x00FFFFFF 418 | container.writeUnsignedInt(((ts >>> 24) & 0xFF) | ((ts << 8) & 0xFFFFFF00)); 419 | container.writeByte(0x00); // StreamID - always 0 420 | container.writeByte(0x00); // StreamID - always 0 421 | container.writeByte(0x00); // StreamID - always 0 422 | 423 | var audioParams:Object = getAudioParameters(name); 424 | if (false === audioParams) { 425 | /* No audio params for this name. */ 426 | ErrorManager.dispatchError(831, [ name ], true); 427 | } 428 | 429 | /* Audio Tag Header */ 430 | container.writeByte(audioParams.format << 4 | audioParams.sampling << 2 | audioParams.depth << 1 | audioParams.type << 0); 431 | 432 | if (0xA === audioParams.format) { 433 | /* A little more setup required if this is AAC */ 434 | container.writeByte(0x1); // AAC Raw 435 | } 436 | 437 | /* Audio Data */ 438 | frame.writeStream(container); 439 | 440 | var size:uint = container.position - start; 441 | 442 | /* Rewind and set the data size in tag header to actual size */ 443 | var dataSize:uint = size - 11; 444 | 445 | container[sizePosition + 0] = (dataSize >> 16 & 0xFF); 446 | container[sizePosition + 1] = (dataSize >> 8 & 0xFF); 447 | container[sizePosition + 2] = (dataSize >> 0 & 0xFF); 448 | 449 | /* End of tag */ 450 | container.writeUnsignedInt(size); 451 | this.lastTimestamp = ts; 452 | 453 | createFLVTag(frame.timestamp, audioParams.duration, true); 454 | } 455 | 456 | public function getLastTimestamp():Number { 457 | return this.lastTimestamp; 458 | } 459 | 460 | public function onNALU(nalu:NALU):void { 461 | switch (nalu.ntype) { 462 | case 1: /* Coded slice of a non-IDR picture */ 463 | case 2: /* Coded slice data partition A */ 464 | case 3: /* Coded slice data partition B */ 465 | case 4: /* Coded slice data partition C */ 466 | case 5: /* Coded slice of an IDR picture */ 467 | /* 1 - 5 are Video Coding Layer (VCL) unit type class (Rec. ITU-T H264 04/2013), and contains video data */ 468 | createVideoTag(nalu); 469 | break; 470 | 471 | case 7: /* Sequence parameter set */ 472 | this.sps = nalu.getPayload(); 473 | createVideoSpecificConfigTag(); 474 | break; 475 | 476 | case 8: /* Picture parameter set */ 477 | this.pps = nalu.getPayload(); 478 | createVideoSpecificConfigTag(); 479 | break; 480 | 481 | default: 482 | /* Unknown NAL unit, skip it */ 483 | /* Return here as nothing was created, and thus nothing should be appended */ 484 | return; 485 | } 486 | } 487 | 488 | public function onAACFrame(aacframe:AACFrame):void { 489 | createAudioTag('mpeg4-generic', aacframe); 490 | } 491 | 492 | public function onPCMAFrame(pcmaframe:PCMAFrame):void { 493 | createAudioTag('pcma', pcmaframe); 494 | } 495 | 496 | private function createFLVTag(timestamp:uint, duration:uint, audio:Boolean):void { 497 | dispatchEvent(new FLVTag(container, timestamp, duration, audio)); 498 | container.position = 0; 499 | container.length = 0; 500 | } 501 | } 502 | } 503 | --------------------------------------------------------------------------------