Overview
AIR does not currently provide an up-to-date implementation for real-time media streaming. KlugMedia adds it as a native extension. Camera, microphone, encoder and echo cancellation run together in one native session, and ActionScript only sends commands and receives status.
Video is published and received as H.264, voice as Opus, both over standard RTMP. Received video is drawn natively on top of the AIR stage, so it stays sharp and smooth even on older devices.
KlugMedia is in daily use in our apps, our proof of concept, on the App Store, Google Play and the Mac.
Features
- Self-contained: one ANE, no core library or dependency ANEs to add
- Camera capture and H.264 publishing in one native session
- Native H.264 receive and display, aspect-correct, with crop-to-fill and rotation
- Two-way Opus voice with hardware echo cancellation, both sides talking at once
- Native camera preview, camera switch, torch and zoom
- Snapshots taken straight from the camera image
- MP4 recording of live streams on the Klug streaming server, picture and sound
- Automatic bitrate adaptation on weak networks
- Adaptive voice buffer that rides out Wi-Fi hiccups and returns to low delay on its own
- On the Mac: switch between the built-in camera and USB cameras in about a second
- Standard RTMP, so it works with ordinary media servers
Streaming server
KlugMedia comes with matching server software, developed together with the extension and tuned for AIR apps: stream handling, reliable reconnects, the two-way voice relay, MP4 recording and live viewing in a web browser.
Supported platforms
iPad apps on the Mac
On Macs with Apple silicon, the iOS build of an AIR app can run as an iPad app. KlugMedia supports this mode with camera, live video and two-way voice, including the built-in FaceTime camera and external USB cameras. One build serves iPhone, iPad and Mac.
Ready for iOS 27 and macOS 27
KlugMedia is tested on iOS 27, iPadOS 27 and macOS 27, on iPhone, iPad, MacBook and Mac mini, and still on iOS 17. Highlights of this release:
- Hardened receive path: stopping and restarting a live stream is safe at any moment, also under the timing of iOS 27
- Stable camera preview on macOS 27 for iPad apps on the Mac, while the camera starts, stops or switches
- Correct orientation on every Mac: built-in and USB cameras stand upright in the preview and at the receiver, on macOS 26 and 27
- Fast camera switch on the Mac between the FaceTime camera and USB cameras, in about a second
- Clear voice on laptop Wi-Fi: an adaptive buffer absorbs the short Wi-Fi bursts typical of MacBooks and shrinks back when the network is calm
Getting started
Add the extension to your application descriptor:
<extensions>
<extensionID>com.klug.media</extensionID>
</extensions>
Request camera and microphone permission before the first start. The extension then owns the capture: do not open an AIR Camera or Microphone at the same time.
Code examples
Five complete flows, taken from the API that runs in our apps every day. Every call returns at once; results arrive as StatusEvents. Full documentation →
import com.klug.petcam.camera.CameraStreamANE; import flash.events.StatusEvent; var media:CameraStreamANE = CameraStreamANE.instance; media.addEventListener( StatusEvent.STATUS, onMedia ); if ( media.isSupported ) { // camera, H.264 encoder and RTMP in one native session media.startPublish( "rtmp://your.server:1935/live", "station-1", 640, 480, // size 12, 800, // fps, kbps 2, // keyframe every 2 s: fast start at the viewer CameraStreamANE.CAMERA_BACK ); } function onMedia( e:StatusEvent ):void { if ( e.code != CameraStreamANE.STATUS_EVENT ) return; if ( e.level == "connected" ) showLive(); else if ( e.level.indexOf( "failed:" ) == 0 ) retryLater( e.level.substr( 7 ) ); else if ( e.level.indexOf( "authError:" ) == 0 ) askForLogin(); }
// the video is decoded natively and drawn in a view above the stage (pixels) media.startReceive( "rtmp://your.server:1935/live", "station-1", 0, 80, stage.stageWidth, stage.stageHeight - 160, 0, CameraStreamANE.RECEIVE_FIT ); // or RECEIVE_CROPFILL media.addEventListener( StatusEvent.STATUS, function( e:StatusEvent ):void { if ( e.code == "receiveStatus" && e.level == "firstFrame" ) hideSpinner(); }); // rotation, a folding phone, fullscreen: move the native view with the stage stage.addEventListener( Event.RESIZE, function( e:Event ):void { media.setReceiveFrame( 0, 80, stage.stageWidth, stage.stageHeight - 160 ); });
import com.klug.petcam.audioecho.*; var voice:OpusVoiceBridge = new OpusVoiceBridge( new NativeOpusCodec(), 48000, 1, 960 ); var link:Red5OpusTransport; var nc:NetConnection = new NetConnection(); nc.addEventListener( NetStatusEvent.NET_STATUS, function( e:NetStatusEvent ):void { if ( e.info.code == "NetConnection.Connect.Success" ) startVoice(); }); nc.connect( "rtmp://your.server:1935/live" ); function startVoice():void { link = new Red5OpusTransport( nc ); link.addEventListener( OpusPacketEvent.PACKET, function( e:OpusPacketEvent ):void { voice.playRemoteOpus( e.packet ); // decode and play the other side }); link.start( "voice_me", "voice_other" ); // my stream, the other side's stream voice.setVoiceProcessing( true ); // hardware echo cancellation EchoCancellationANE.instance.setDuck( false ); // both sides may talk at once voice.start(); addEventListener( Event.ENTER_FRAME, sendVoice ); } function sendVoice( e:Event ):void { var packet:ByteArray; while ( ( packet = voice.readLocalOpus() ) != null ) // every finished 20 ms frame link.sendPacket( packet ); }
media.showPreview( 20, 100, 320, 240, CameraStreamANE.CAMERA_FRONT ); // aiming preview media.switchCamera(); // front / back, or built-in / USB camera on the Mac if ( media.hasTorch ) media.setTorch( true ); media.setCameraZoom( 4 ); // 0..8 = 1.0x..2.0x, the stream is zoomed too media.setVideoBitrate( 500 ); // change the bitrate while live media.addEventListener( StatusEvent.STATUS, function( e:StatusEvent ):void { if ( e.code == "snapshotStatus" && e.level.indexOf( "saved:" ) == 0 ) showPhoto( e.level.substr( 6 ) ); // absolute path of the JPEG }); media.captureSnapshot( 640, 480 ); media.hidePreview(); // native views sit above the stage: hide them before dialogs
function stopAll():void { removeEventListener( Event.ENTER_FRAME, sendVoice ); voice.stop(); link.stop(); media.stopReceive(); media.stopPublish(); // always stop before publishing the same name again } // safe at any moment, also while a stream is still connecting stopAll(); media.startPublish( "rtmp://your.server:1935/live", "station-1" );
API overview
| isSupported | True where the native media session is available. |
| startPublish(…) | Starts the camera and publishes H.264 over RTMP: size, fps, bitrate, keyframe interval, camera, optional RTMP login. |
| stopPublish() | Stops publishing; the camera is released unless the preview is still shown. |
| startReceive(…) | Plays a remote stream natively inside a rectangle of the stage: aspect-fit or crop-to-fill with rotation (stretch on Android). |
| setReceiveFrame(x, y, w, h) | Moves and resizes the native video, for rotation, folding phones and fullscreen. |
| stopReceive() | Stops playback and removes the native view. |
| showPreview(…) / hidePreview() | Native camera preview over the stage, alone or beside a live publish. |
| switchCamera() | Switches to the next camera while streaming: front and back on iPhone and iPad, built-in and USB cameras on the Mac. |
| setTorch(on) / hasTorch | Camera light while streaming, and whether the device has one. |
| setCameraZoom(step) | Real camera zoom on iOS and Android, 0..8 for 1.0x to 2.0x; the stream is zoomed too. |
| setVideoBitrate(kbps) | Changes the encoder bitrate while live. |
| captureSnapshot(w, h) | Saves a JPEG from the live camera image. |
| isiOSAppOnMac | True when the iOS build runs as an iPad app on an Apple silicon Mac. |
| OpusVoiceBridge | Two-way voice: start, stop, echo cancellation, speaker, gain, playback buffer. |
| Red5OpusTransport | Carries the Opus packets over your RTMP connection on two dedicated streams. |
Status events
| cameraStatus | connecting · connected · disconnected · failed:<reason> · authError:<reason> · newBitrate:<rate> · authSuccess (Android) |
| receiveStatus | playing · firstFrame · disconnected · streamEnded and failed:<reason> (iOS, macOS) |
| snapshotStatus | saved:<path> · failed:<reason> |
Buy KlugMedia
A yearly licence for one AIR application, with all updates for 12 months. After payment you receive the extension, the ActionScript library and the documentation by e-mail, usually within one business day. Licence and terms
- Licence for one AIR application
- All updates for 12 months
- Answer by e-mail within 7 days
- Licence for one AIR application
- All updates for 12 months
- Answer by e-mail within 48 hours
Requirements
| AIR SDK | 51.x (HARMAN) |
| iOS | iOS 17 or later |
| Android | Android 6.0 (API 23) or later |
| macOS | macOS 11 or later |
| iPad apps on the Mac | Apple silicon, macOS 26 or later |
| Permissions | Camera and microphone |
| Server | Any RTMP server; the matching Klug streaming server is recommended |