Hypy 앱 - App Store

라이브스트리밍 테스트영상

라이브스트리밍 테스트영상

라이브 스트리밍 네트워크 예외처리

라이브 스트리밍 네트워크 예외처리

VOD 실시간 채팅 기능 테스트

VOD 실시간 채팅 기능 테스트


✅  핵심 문제 해결 사례

1) 필터→렌더→송출 파이프라인 (CoreMedia Adapter)

문제(Problem)

GPUPixel 필터 처리 결과는 CVPixelBuffer + CMTime 형태로 나오지만, HaishinKit 송출 입력은 CMSampleBuffer를 요구해 필터 결과를 그대로 RTMP로 주입할 수 없었습니다.

해결(Solution)

필터 산출물(CVPixelBuffer + CMTime)을 CMSampleBuffer로 변환하는 CoreMedia 어댑터를 구현하고, 변환된 프레임을 MediaMixer.append(sampleBuffer)로 주입해 필터 적용 화면이 그대로 송출되도록 파이프라인을 구성했습니다.

코드 스니펫(Code Snippet)

— offscreen 프레임(CVPixelBuffer+CMTime)을 GPUPixel로 전달

// IdolLiveDevice.swift
func videoOffscreenRenderCaptureOutput(
    pixelBuffer: CVPixelBuffer,
    time: CMTime,
    position: AVCaptureDevice.Position
) {
    guard position == currentCameraPosition else { return }

    if position == .front {
        frontGpuPixelManager?.processImageBuffer(imageBuffer: pixelBuffer, timestamp: time)
    } else {
        backGpuPixelManager?.processImageBuffer(imageBuffer: pixelBuffer, timestamp: time)
    }
}

— PixelBuffer+Time → CMSampleBuffer 변환 후 MediaMixer.append로 송출 주입

// IdolLiveGpuPixel.swift
func gpuPixelCaptureOutput(
    pixelBuffer: CVPixelBuffer,
    time: CMTime,
    position: AVCaptureDevice.Position
) {
    cameraMananger?.appendFrame(
        pixelBuffer: pixelBuffer,
        sourcePostion: position,
        timestamp: time
    )

    guard position == currentCameraPosition else { return }

    if let sampleBuffer = sampleBuffer(from: pixelBuffer, presentationTime: time, frameRate: 30) {
        Task { await haishinkitManager?.mixer?.append(sampleBuffer) }
    }
}

func sampleBuffer(
    from pixelBuffer: CVPixelBuffer,
    presentationTime: CMTime,
    frameRate: Int = 30
) -> CMSampleBuffer? {
    var sampleBuffer: CMSampleBuffer?

    let frameDuration = CMTime(
        value: CMTimeValue(1_000_000_000 / frameRate),
        timescale: 1_000_000_000
    )

    var timingInfo = CMSampleTimingInfo(
        duration: frameDuration,
        presentationTimeStamp: presentationTime,
        decodeTimeStamp: presentationTime
    )

    var formatDesc: CMVideoFormatDescription?
    let status = CMVideoFormatDescriptionCreateForImageBuffer(
        allocator: kCFAllocatorDefault,
        imageBuffer: pixelBuffer,
        formatDescriptionOut: &formatDesc
    )
    guard status == noErr, let formatDesc else { return nil }

    let bufferStatus = CMSampleBufferCreateForImageBuffer(
        allocator: kCFAllocatorDefault,
        imageBuffer: pixelBuffer,
        dataReady: true,
        makeDataReadyCallback: nil,
        refcon: nil,
        formatDescription: formatDesc,
        sampleTiming: &timingInfo,
        sampleBufferOut: &sampleBuffer
    )
    guard bufferStatus == noErr else { return nil }

    return sampleBuffer
}