개요
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 카메라를 약 1초 만에 전환
- 표준 RTMP이므로 일반적인 미디어 서버와 함께 동작
스트리밍 서버
KlugMedia에는 확장과 함께 개발되어 AIR 앱에 맞게 조정된 매칭 서버 소프트웨어가 포함됩니다. 스트림 처리, 안정적인 재연결, 양방향 음성 릴레이, MP4 녹화, 웹 브라우저 실시간 시청을 지원합니다.
지원 플랫폼
Mac의 iPad 앱
Apple silicon을 탑재한 Mac에서는 AIR 앱의 iOS 빌드를 iPad 앱으로 실행할 수 있습니다. KlugMedia는 내장 FaceTime 카메라와 외장 USB 카메라를 포함해 카메라, 실시간 영상, 양방향 음성으로 이 모드를 지원합니다. 하나의 빌드로 iPhone, iPad, Mac을 모두 지원합니다.
iOS 27 및 macOS 27 대응
KlugMedia는 iPhone, iPad, MacBook, Mac mini의 iOS 27, iPadOS 27, macOS 27에서 테스트되었으며 iOS 17에서도 계속 테스트됩니다. 이번 릴리스의 주요 사항:
- 강화된 수신 경로: 라이브 스트림의 중지와 재시작이 iOS 27의 타이밍에서도 언제든 안전합니다
- macOS 27에서 안정적인 카메라 미리보기: Mac의 iPad 앱에서 카메라가 시작, 중지, 전환되는 동안에도 유지됩니다
- 모든 Mac에서 올바른 방향: macOS 26 및 27에서 내장 카메라와 USB 카메라가 미리보기와 수신 측 모두에서 똑바로 표시됩니다
- Mac에서 빠른 카메라 전환: FaceTime 카메라와 USB 카메라 사이를 약 1초 만에 전환합니다
- 노트북 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를 퍼블리싱합니다: 크기, fps, 비트레이트, 키프레임 간격, 카메라, 선택적 RTMP 로그인. |
| stopPublish() | 퍼블리싱을 중지합니다. 미리보기가 아직 표시 중이 아니면 카메라가 해제됩니다. |
| startReceive(…) | 스테이지의 사각형 영역 안에서 원격 스트림을 네이티브로 재생합니다: 비율 유지 또는 회전을 포함한 크롭 채우기(Android에서는 늘이기). |
| setReceiveFrame(x, y, w, h) | 회전, 폴더블 폰, 전체 화면을 위해 네이티브 영상을 이동하고 크기를 조정합니다. |
| stopReceive() | 재생을 중지하고 네이티브 뷰를 제거합니다. |
| showPreview(…) / hidePreview() | 스테이지 위의 네이티브 카메라 미리보기이며, 단독으로 또는 라이브 퍼블리싱과 함께 사용할 수 있습니다. |
| switchCamera() | 스트리밍 중 다음 카메라로 전환합니다: iPhone과 iPad에서는 전면/후면, Mac에서는 내장 카메라와 USB 카메라. |
| setTorch(on) / hasTorch | 스트리밍 중 카메라 조명 제어, 그리고 기기에 조명이 있는지 여부. |
| setCameraZoom(step) | iOS와 Android에서의 실제 카메라 줌, 1.0x~2.0x에 해당하는 0..8, 스트림도 함께 줌됩니다. |
| setVideoBitrate(kbps) | 라이브 중에 인코더 비트레이트를 변경합니다. |
| captureSnapshot(w, h) | 라이브 카메라 이미지에서 JPEG를 저장합니다. |
| isiOSAppOnMac | iOS 빌드가 Apple silicon Mac에서 iPad 앱으로 실행될 때 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 애플리케이션 1개에 대한 연간 라이선스이며 12개월간 모든 업데이트를 제공합니다. 결제 후 확장, ActionScript 라이브러리, 문서를 이메일로 보내 드리며, 보통 영업일 기준 1일 이내입니다. 라이선스 및 약관
요구 사항
| AIR SDK | 51.x (HARMAN) |
| iOS | iOS 17 이상 |
| Android | Android 6.0 (API 23) 이상 |
| macOS | macOS 11 이상 |
| Mac의 iPad 앱 | Apple silicon, macOS 26 이상 |
| 권한 | 카메라 및 마이크 |
| 서버 | 모든 RTMP 서버, 매칭되는 Klug 스트리밍 서버 권장 |