Dunfey · Hotel WWDC as data, est. 1983
Front desk everything
Years
Topics

2025 Audio & VideoSpatial Computing

WWDC25 · 20 min · Audio & Video / Spatial Computing

Learn about the Apple Projected Media Profile

Dive into the Apple Projected Media Profile (APMP) and see how APMP enables 180º/360º and Wide FoV projections in QuickTime and MP4 files using Video Extended Usage signaling. We’ll provide guidance on using OS-provided frameworks and tools to convert, read/write, edit, and encode media containing APMP. And we’ll review Apple Positional Audio Codec’s (APAC) capabilities for creating and delivering spatial audio content for the most immersive experiences.

Watch at developer.apple.com ↗

Transcript all transcripts

Chapters

  • 0:00 — Introduction
  • 1:12 — Non-rectilinear video fundamentals
  • 3:42 — Apple Projected Media Profile specification
  • 5:59 — APMP content capturing and workflows
  • 8:33 — Asset conversion capabilities
  • 10:45 — APMP video reading
  • 11:47 — APMP video editing
  • 13:42 — APMP video publishing
  • 16:14 — Apple Positional Audio Codec

Code shown on screen · 6 snippets

Recognize spherical v1/v2 equirectangular content swift · at 8:58 ↗
// Convert spherical v1/v2 RFC 180/360 equirectangular content

import AVFoundation

func wasConvertedFromSpherical(url: URL) -> Bool {
	let assetOptions = [AVURLAssetShouldParseExternalSphericalTagsKey: true]
	let urlAsset = AVURLAsset(url: url, options: assetOptions)
	
	// simplified for sample, assume first video track
	let track = try await urlAsset.loadTracks(withMediaType: .video).first!
	
	// Retrieve formatDescription from video track, simplified for sample assume first format description
	let formatDescription = try await videoTrack.load(.formatDescriptions).first
	
	// Detect if formatDescription includes extensions synthesized from spherical
	let wasConvertedFromSpherical = formatDescription.extensions[.convertedFromExternalSphericalTags]
	
	return wasConvertedFromSpherical
}
Convert wide FOV content from supported cameras swift · at 9:54 ↗
// Convert wide-FOV content from recognized camera models
import ImmersiveMediaSupport

func upliftIntoParametricImmersiveIfPossible(url: URL) -> AVMutableMovie {
	let movie = AVMutableMovie(url: url)

	let assetInfo = try await ParametricImmersiveAssetInfo(asset: movie)
	if (assetInfo.isConvertible) {
		guard let newDescription = assetInfo.requiredFormatDescription else {
			fatalError("no format description for convertible asset")
		}
		let videoTracks = try await movie.loadTracks(withMediaType: .video)
		guard let videoTrack = videoTracks.first,
			  let currentDescription = try await videoTrack.load(.formatDescriptions).first
		else {
      fatalError("missing format description for video track")
		}
		// presumes that format already compatible for intended use case (delivery or production)
    // for delivery then if not already HEVC should transcode for example
		videoTrack.replaceFormatDescription(currentDescription, with: newDescription)
	}
  return movie
}
Recognize Projected & Immersive Video swift · at 10:58 ↗
// Determine if an asset contains any tracks with nonRectilinearVideo and if so, whether any are AIV
import AVFoundation

func classifyProjectedMedia( movieURL: URL ) async -> (containsNonRectilinearVideo: Bool, containsAppleImmersiveVideo: Bool) {
	
	let asset = AVMovie(url: movieURL)
	let assistant = AVAssetPlaybackAssistant(asset: asset)
	let options = await assistant.playbackConfigurationOptions
	// Note contains(.nonRectilinearProjection) is true for both APMP & AIV, while contains(.appleImmersiveVideo) is true only for AIV
	return (options.contains(.nonRectilinearProjection), options.contains(.appleImmersiveVideo))
}
Perform projection or viewPacking processing swift · at 11:22 ↗
import AVFoundation
import CoreMedia

// Perform projection or viewPacking specific processing
func handleProjectionAndViewPackingKind(_ movieURL: URL) async throws {
	
	let movie = AVMovie(url: movieURL)
	let track = try await movie.loadTracks(withMediaType: .video).first!
	let mediaCharacteristics = try await track.load(.mediaCharacteristics)
	
	// Check for presence of non-rectilinear projection
	if mediaCharacteristics.contains(.indicatesNonRectilinearProjection) {
		let formatDescriptions = try await track.load(.formatDescriptions)
		for formatDesc in formatDescriptions {
			if let projectionKind = formatDesc.extensions[.projectionKind] {
				if projectionKind == .projectionKind(.equirectangular) {
					// handle equirectangular (360) video
				} else if projectionKind == .projectionKind(.halfEquirectangular) {
					// handle 180 video
				} else if projectionKind == .projectionKind(.parametricImmersive) {
					// handle parametric wfov video
				} else if projectionKind == .projectionKind(.appleImmersiveVideo) {
					// handle AIV
				}
			}
			if let viewPackingKind = formatDesc.extensions[.viewPackingKind] {
				if viewPackingKind == .viewPackingKind(.sideBySide) {
					// handle side by side
				} else if viewPackingKind == .viewPackingKind(.overUnder) {
					// handle over under
				}
			}
		}
	}
}
Specify outputBufferDescription for a stereoscopic pair swift · at 12:51 ↗
var config = try await AVVideoComposition.Configuration(for: asset)
	
	config.outputBufferDescription = [[.stereoView(.leftEye)], [.stereoView(.rightEye)]]

	let videoComposition = AVVideoComposition(configuration: config)
Finish an asyncVideoCompositionRequest with tagged buffers swift · at 13:01 ↗
func startRequest(_ asyncVideoCompositionRequest: AVAsynchronousVideoCompositionRequest) {
	var taggedBuffers: [CMTaggedDynamicBuffer] = []
	let MVHEVCLayerIDs = [0, 1]
	let eyes: [CMStereoViewComponents] = [.leftEye, .rightEye]
	
	for (layerID, eye) in zip(MVHEVCLayerIDs, eyes) {
		// take a monoscopic image and convert it to a z=0 stereo image with identical content for each eye
		let pixelBuffer = asyncVideoCompositionRequest.sourceReadOnlyPixelBuffer(byTrackID: 0)
		
		let tags: [CMTag] = [.videoLayerID(Int64(layerID)), .stereoView(eye)]
		let buffer = CMTaggedDynamicBuffer(tags: tags, content: .pixelBuffer(pixelBuffer!))
		taggedBuffers.append(buffer)
	}
	asyncVideoCompositionRequest.finish(withComposedTaggedBuffers: taggedBuffers)
}

Resources