← 所有扩展

原生扩展 · 旗舰

KlugMedia

com.klug.media

无需 APM。下载即用,再简单不过。

摄像头、双向语音和硬件回声消除集于同一个原生会话,通过标准 RTMP 发布和接收 H.264 与 Opus。

iOSAndroidmacOS 已投入生产 €99 / 年起 · 购买

概览

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 录制以及在网页浏览器中实时观看。

服务器在概览页中有介绍:流媒体服务器。

支持的平台

iOS arm64
Android arm64 · armv7
macOS Apple 芯片 · Intel
Mac 上的 iPad 应用 Apple 芯片 · macOS 26 和 27

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 突发,并在网络平稳时缩小

入门

将扩展添加到您的应用描述文件中:

app.xml
<extensions>
    <extensionID>com.klug.media</extensionID>
</extensions>

请在首次启动前申请摄像头和麦克风权限。此后由扩展接管采集:请勿同时打开 AIR 的 Camera 或 Microphone。

代码示例

五个完整流程,取自我们应用每天运行的 API。每次调用都会立即返回;结果以 StatusEvent 的形式到达。完整文档 →

1 · 发布摄像头流
Station.as
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();
}
2 · 接收并跟随布局
Viewer.as
// 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 );
});
3 · 带回声消除的双向语音
Voice.as
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 );
}
语音数据包作为数据消息在各自独立的一对流上传输。Klug 流媒体服务器开箱即用地中继这些数据包;其他 RTMP 服务器需要将 NetStream.send 转发给订阅者。
4 · 预览、摄像头切换、补光灯、缩放、快照
Controls.as
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
5 · 干净地停止与重启
Session.as
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 数据包。

状态事件

cameraStatusconnecting · connected · disconnected · failed:<reason> · authError:<reason> · newBitrate:<rate> · authSuccess(Android)
receiveStatusplaying · firstFrame · disconnected · streamEnded 和 failed:<reason>(iOS、macOS)
snapshotStatussaved:<path> · failed:<reason>

购买 KlugMedia

一个 AIR 应用的年度许可,含 12 个月内的所有更新。付款后,您将通过电子邮件收到扩展、ActionScript 库和文档,通常在一个工作日内。许可与条款

基础版
€99 / 年
  • 一个 AIR 应用的许可
  • 12 个月内的所有更新
  • 不含支持
购买基础版
标准版
€349 / 年
  • 一个 AIR 应用的许可
  • 12 个月内的所有更新
  • 7 天内通过电子邮件答复
购买标准版
优先版
€699 / 年
  • 一个 AIR 应用的许可
  • 12 个月内的所有更新
  • 48 小时内通过电子邮件答复
购买优先版
税费在结账时由 Link (Stripe) 计算。您需要在结账时输入 AIR 应用的应用 ID。

要求

AIR SDK51.x (HARMAN)
iOSiOS 17 或更高版本
AndroidAndroid 6.0 (API 23) 或更高版本
macOSmacOS 11 或更高版本
Mac 上的 iPad 应用Apple 芯片,macOS 26 或更高版本
权限摄像头和麦克风
服务器任意 RTMP 服务器;推荐使用配套的 Klug 流媒体服务器