概览
AIR 目前没有提供最新的实时媒体流实现。KlugMedia 以原生扩展的形式补上了这一块。摄像头、麦克风、编码器和回声消除一起在同一个原生会话中运行,ActionScript 只负责发送命令和接收状态。
视频以 H.264 发布和接收,语音使用 Opus,二者均通过标准 RTMP 传输。接收到的视频以原生方式绘制在 AIR 舞台之上,因此即使在较旧的设备上也能保持清晰流畅。
KlugMedia 每天都在我们的应用中使用,这就是我们的概念验证,已上架 App Store、Google Play 和 Mac。
特性
- 自成一体:一个 ANE,无需添加核心库或依赖 ANE
- 在同一个原生会话中完成摄像头采集和 H.264 推流
- 原生 H.264 接收与显示,保持正确宽高比,支持裁剪填充和旋转
- 带硬件回声消除的双向 Opus 语音,双方可同时说话
- 原生摄像头预览、摄像头切换、补光灯和缩放
- 直接从摄像头画面获取快照
- 在 Klug 流媒体服务器上录制实时流的 MP4,含画面与声音
- 在弱网络下自动调整码率
- 自适应语音缓冲,可平稳度过 Wi-Fi 抖动,并自动恢复到低延迟
- 在 Mac 上:约一秒内即可在内置摄像头和 USB 摄像头之间切换
- 标准 RTMP,可与常见的媒体服务器配合使用
流媒体服务器
KlugMedia 附带配套的服务器软件,与扩展一同开发,并针对 AIR 应用调优:流处理、可靠的重新连接、双向语音中继、MP4 录制以及在网页浏览器中实时观看。
支持的平台
Mac 上的 iPad 应用
在搭载 Apple 芯片的 Mac 上,AIR 应用的 iOS 构建可作为 iPad 应用运行。KlugMedia 支持此模式下的摄像头、实时视频和双向语音,包括内置 FaceTime 摄像头和外接 USB 摄像头。一次构建即可同时适用于 iPhone、iPad 和 Mac。
已为 iOS 27 和 macOS 27 做好准备
KlugMedia 已在 iOS 27、iPadOS 27 和 macOS 27 上,以及 iPhone、iPad、MacBook 和 Mac mini 上测试,同时仍支持 iOS 17。此版本的亮点:
- 加固的接收路径:随时停止并重新启动实时流都是安全的,在 iOS 27 的时序下同样如此
- macOS 27 上稳定的摄像头预览,适用于 Mac 上的 iPad 应用,包括摄像头启动、停止或切换时
- 在每台 Mac 上方向正确:在 macOS 26 和 27 上,内置和 USB 摄像头在预览及接收端都保持正立
- Mac 上快速切换摄像头:在 FaceTime 摄像头和 USB 摄像头之间切换仅需约一秒
- 笔记本 Wi-Fi 下清晰的语音:自适应缓冲可吸收 MacBook 常见的短时 Wi-Fi 突发,并在网络平稳时缩小
入门
将扩展添加到您的应用描述文件中:
<extensions>
<extensionID>com.klug.media</extensionID>
</extensions>
请在首次启动前申请摄像头和麦克风权限。此后由扩展接管采集:请勿同时打开 AIR 的 Camera 或 Microphone。
代码示例
五个完整流程,取自我们应用每天运行的 API。每次调用都会立即返回;结果以 StatusEvent 的形式到达。完整文档 →
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 概览
| isSupported | 在原生媒体会话可用的平台上为 True。 |
| startPublish(…) | 启动摄像头并通过 RTMP 发布 H.264:尺寸、帧率、码率、关键帧间隔、摄像头、可选的 RTMP 登录。 |
| stopPublish() | 停止发布;除非仍在显示预览,否则摄像头会被释放。 |
| startReceive(…) | 在舞台的某个矩形区域内以原生方式播放远程流:适应宽高比或裁剪填充并旋转(Android 上为拉伸)。 |
| setReceiveFrame(x, y, w, h) | 移动并调整原生视频的大小,用于旋转、折叠手机和全屏。 |
| stopReceive() | 停止播放并移除原生视图。 |
| showPreview(…) / hidePreview() | 在舞台上方显示原生摄像头预览,可单独显示,也可与实时推流并排显示。 |
| switchCamera() | 在推流时切换到下一个摄像头:iPhone 和 iPad 上为前后摄像头,Mac 上为内置和 USB 摄像头。 |
| setTorch(on) / hasTorch | 推流时的摄像头补光灯,以及设备是否配有补光灯。 |
| setCameraZoom(step) | iOS 和 Android 上的真实摄像头缩放,0..8 对应 1.0x 到 2.0x;推流画面同样会被缩放。 |
| setVideoBitrate(kbps) | 在直播过程中更改编码器码率。 |
| captureSnapshot(w, h) | 从实时摄像头画面保存一张 JPEG。 |
| isiOSAppOnMac | 当 iOS 构建作为 iPad 应用在 Apple 芯片 Mac 上运行时为 True。 |
| OpusVoiceBridge | 双向语音:启动、停止、回声消除、扬声器、增益、播放缓冲。 |
| Red5OpusTransport | 通过您的 RTMP 连接,在两路专用流上传输 Opus 数据包。 |
状态事件
| cameraStatus | connecting · connected · disconnected · failed:<reason> · authError:<reason> · newBitrate:<rate> · authSuccess(Android) |
| receiveStatus | playing · firstFrame · disconnected · streamEnded 和 failed:<reason>(iOS、macOS) |
| snapshotStatus | saved:<path> · failed:<reason> |
购买 KlugMedia
一个 AIR 应用的年度许可,含 12 个月内的所有更新。付款后,您将通过电子邮件收到扩展、ActionScript 库和文档,通常在一个工作日内。许可与条款
要求
| AIR SDK | 51.x (HARMAN) |
| iOS | iOS 17 或更高版本 |
| Android | Android 6.0 (API 23) 或更高版本 |
| macOS | macOS 11 或更高版本 |
| Mac 上的 iPad 应用 | Apple 芯片,macOS 26 或更高版本 |
| 权限 | 摄像头和麦克风 |
| 服务器 | 任意 RTMP 服务器;推荐使用配套的 Klug 流媒体服务器 |