How to embed VideoPlayer

Let’s take a look at the simplest implementation of a video player and the result you can achieve.

VideoPlayer(player: AVPlayer(url: verticalVideo))
    .clipShape(RoundedRectangle(cornerRadius: 10))
bug preview bug preview

You may notice black bars. These appear because the player tries to fill all the available space. Using modifiers like .scaledToFill(), .scaledToFit(), or .aspectRatio(contentMode:) will not change how the video is displayed within the VideoPlayer. These modifiers only adjust the outer frame of the VideoPlayer, leaving the black areas intact. As of late 2024, there are no obvious ways to manage the VideoPlayer settings directly.An alternative approach is to set a specific aspect ratio for the VideoPlayer frame. To determine the aspect ratio, I suggest using an extension.

extension URL {
    func getVideoAspectRatio() async -> CGFloat? {
        guard let track = try? await AVURLAsset(url: self).loadTracks(withMediaType: .video).first,
              let resolution = try? await track.load(.naturalSize) else {
            assertionFailure()
            return nil
        }
        return resolution.width / resolution.height
    }
}

An example of how to use it:

struct VideoPlayerView: View {
    
    let videoURL: URL
    @State var aspectRatio: CGFloat?
    
    var body: some View {
        playerView()
            .onAppear {
                setupAspectRatio()
            }
    }
    
    func playerView() -> some View {
        VideoPlayer(player: AVPlayer(url: videoURL))
            .aspectRatio(aspectRatio, contentMode: .fit)
            .clipShape(RoundedRectangle(cornerRadius: 5))
    }
    
    func setupAspectRatio() {
        Task {
            guard let aspectRatio = await videoURL.getVideoAspectRatio() else { return }
            self.aspectRatio = aspectRatio
        }
    }
    
}

The result:

bug preview

It looks good—now the VideoPlayer frame matches the video dimensions. Let’s test with a vertical video.

It looks incorrect. Let’s investigate what’s happening. Place a breakpoint on the line return resolution.width / resolution.height in the extension.

The resolution indicates that the video is in portrait mode, even though it’s actually not. This happens because the video metadata includes information instructing the video to be displayed rotated. Let’s add another extension to determine if the video has been rotated.

extension URL {
    func getVideoAspectRatio() async -> CGFloat? {
        guard let track = try? await AVURLAsset(url: self).loadTracks(withMediaType: .video).first,
              let resolution = try? await track.load(.naturalSize) else {
            assertionFailure()
            return nil
        }
        if await track.isRotated() {
            return resolution.height / resolution.width
        }
        return resolution.width / resolution.height
    }
}

extension AVAssetTrack {
    /// checks if video is rotated, which may result in a changed aspect ratio, for example from 16:9 to 9:16
    func isRotated() async  -> Bool {
        guard let transform = try? await self.load(.preferredTransform) else { return false }
        let isRotated = abs(transform.b) == 1 && abs(transform.c) == 1
        return isRotated
    }
}

Now the aspect ratio will be determined correctly, taking the metadata into account. Moving forward, it will be quite simple to integrate full-frame video into the app’s interface.