Thanks for being a part of WWDC25!

How did we do? We’d love to know your thoughts on this year’s conference. Take the survey here

H.265 Decoding with VideoToolBox

I am creating an app that decodes H.265 elementary streams on iOS.

I use VideoToolBox to decode from H.265 to NV12.

The decoded data is enqueued in the CMSampleBufferDisplayLayer as a CMSampleBuffer.

However, nothing is displayed in the VideoPlayerView. It remains black.

The decoding in VideoToolBox is successful. I confirmed this by saving the NV12 data in the CMSampleBuffer to a file and displaying it using a tool.

Why is nothing displayed in the VideoPlayerView?

I can provide other source code as well.

//
//  ContentView.swift
//  H265Decoder
//
//  Created by Kohshin Tokunaga on 2025/02/15.
//

import SwiftUI

struct ContentView: View {
    var body: some View {
        VStack {
            Text("H.265 Player (temp.h265)")
                .font(.headline)
            VideoPlayerView()
                .frame(width: 360, height: 640) // Adjust or make it responsive for iOS
        }
        .padding()
    }
}

#Preview {
    ContentView()
}
//
//  VideoPlayerView.swift
//  H265Decoder
//
//  Created by Kohshin Tokunaga on 2025/02/15.
//

import SwiftUI
import AVFoundation

struct VideoPlayerView: UIViewRepresentable {
    
    // Return an H265Player as the coordinator, and start playback there.
    func makeCoordinator() -> H265Player {
        H265Player()
    }
    
    func makeUIView(context: Context) -> UIView {
        let uiView = UIView(frame: .zero)
        
        // Base layer for attaching sublayers
        uiView.backgroundColor = .black // Screen background color (for iOS)
        
        // Create the display layer and add it to uiView.layer
        let displayLayer = context.coordinator.displayLayer
        displayLayer.frame = uiView.bounds
        displayLayer.backgroundColor = UIColor.clear.cgColor
        
        uiView.layer.addSublayer(displayLayer)
        
        // Start playback
        context.coordinator.startPlayback()
        
        return uiView
    }
    
    func updateUIView(_ uiView: UIView, context: Context) {
        // Reset the frame of the AVSampleBufferDisplayLayer when the view's size changes.
        let displayLayer = context.coordinator.displayLayer
        displayLayer.frame = uiView.layer.bounds
        
        // Optionally update the layer's background color, etc.
        uiView.backgroundColor = .black
        displayLayer.backgroundColor = UIColor.clear.cgColor
        
        // Flush transactions if necessary
        CATransaction.flush()
    }
}
//
//  H265Player.swift
//  H265Decoder
//
//  Created by Kohshin Tokunaga on 2025/02/15.
//

import Foundation
import AVFoundation
import CoreMedia

class H265Player: NSObject, VideoDecoderDelegate {
    
    let displayLayer = AVSampleBufferDisplayLayer()
    private var decoder: H265Decoder?
    
    override init() {
        super.init()
        
        // Initial configuration for the display layer
        displayLayer.videoGravity = .resizeAspect
        
        // Initialize the decoder (delegate = self)
        decoder = H265Decoder(delegate: self)
        
        // For simple playback, set isBaseline to true
        decoder?.isBaseline = true
    }
    
    func startPlayback() {
        // Load the file "cars_320x240.h265"
        guard let url = Bundle.main.url(forResource: "temp2", withExtension: "h265") else {
            print("File not found")
            return
        }
        do {
            let data = try Data(contentsOf: url)
            // Set FPS and video size as needed
            let packet = VideoPacket(data: data,
                                     type: .h265,
                                     fps: 30,
                                     videoSize: CGSize(width: 1080, height: 1920))
            
            // Decode as a single packet
            decoder?.decodeOnePacket(packet)
            
        } catch {
            print("Failed to load file: \(error)")
        }
    }
    
    // MARK: - VideoDecoderDelegate
    func decodeOutput(video: CMSampleBuffer) {
        // When decoding is complete, send the output to AVSampleBufferDisplayLayer
        displayLayer.enqueue(video)
    }
    
    func decodeOutput(error: DecodeError) {
        print("Decoding error: \(error)")
    }
}
Answered by KohshinTokunaga in 826263022

I uploaded the project to github.

https://github.com/kohshin1977/H265Decoder

Accepted Answer

I uploaded the project to github.

https://github.com/kohshin1977/H265Decoder

Hello @KohshinTokunaga , thank you for your post, and thank you for sharing code snippets and a test project. The snippets and project vary a great deal in the sense that the former leverages AVSampleBufferDisplayLayer, whereas the latter relies on Metal's MTKView.

Both approches ultimately place decoded video data in instances of CMSampleBuffer. Therefore AVSampleBufferDisplayLayer should be able to handle basic playback, provided there are no decoding errors.

Looking at the snippets you shared, it seems there might be components missing in your implementation. In particular, AVSampleBufferDisplayLayer needs to be added to an instance of AVSampleBufferRenderSynchronizer.

There are different strategies for enqueing sample buffers to the display layer. One of them is by providing a callback to requestMediaDataWhenReady(on:using:). However, playback will not start until you set a nonzero rate on the render synchronizer.

Please see the Implementing flexible enhanced buffering for your content for an example implementation.

H.265 Decoding with VideoToolBox
 
 
Q