With Audio Passthrough on, every (E-)AC-3 track on Apple TV dropped 100-220ms of audio every 1.79s. The system pipeline behind the compressed AVPlayer path reads unboundedly far ahead of the playhead and consumes its buffer in fixed ~1.8s quanta; the AO's 2s feed lead meant each refill overran the write head and the renderer skipped the missing audio to stay on clock. Bitrate- and route-independent, and invisible to the AO: item status, time control, and the feed margin all stayed clean while the sink output gapped (#1300, #1776). MPVKit 1.0.25 feeds the elementary stream 8s ahead of the observed playhead -- 4x the pipeline's refill quantum -- sizes the ES window from the configured lead, exposes it as --ao-avfoundation-compressed-lead, and keeps waiting during preroll while a slow source (a realtime-pinned server transcode) is still making priming progress instead of abandoning Atmos for PCM. Verified on an Apple TV 4K gen 3 with an HDMI audio capture rig against EAC3 768k and 640k streams: 21 dropouts/min on 2.15.0, zero across 10.7 minutes with the new lead; a 997Hz sine encoded as EAC3 through the full passthrough path is statistically identical to first-party AVKit playback of the same tone; a rate-limited server at 0.95x realtime now prerolls late on the compressed path instead of downgrading, and once the lead is filled mpv's cache pauses absorb the deficit with the margin intact.
137 lines
4.4 KiB
Ruby
Executable File
137 lines
4.4 KiB
Ruby
Executable File
#!/usr/bin/env ruby
|
|
|
|
require 'fileutils'
|
|
require 'json'
|
|
require 'minitest/autorun'
|
|
require 'open3'
|
|
require 'rbconfig'
|
|
require 'tmpdir'
|
|
require 'xcodeproj'
|
|
|
|
class WireMpvTest < Minitest::Test
|
|
SOURCE_NAMES = %w[
|
|
MpvPlayerCoreBase.swift
|
|
MpvPlayerPluginShared.swift
|
|
MpvPlayerCore.swift
|
|
MpvPlayerPlugin.swift
|
|
MpvPipController.swift
|
|
MpvAudioPlayerCore.swift
|
|
MpvAudioPlayerPlugin.swift
|
|
].freeze
|
|
AFFECTED_NAMES = %w[
|
|
MpvAudioPlayerCore.swift
|
|
MpvAudioPlayerPlugin.swift
|
|
].freeze
|
|
MPVKIT_PIN = {
|
|
'location' => 'https://github.com/edde746/MPVKit',
|
|
'revision' => '94108e2976b80e8a4b465c6bf72ea6934c5ebde4',
|
|
'version' => '1.0.25',
|
|
}.freeze
|
|
|
|
def setup
|
|
@temporary_root = Dir.mktmpdir('wire-mpv-test')
|
|
@tvos_root = File.join(@temporary_root, 'tvos')
|
|
FileUtils.mkdir_p(File.join(@tvos_root, 'scripts'))
|
|
FileUtils.cp_r(File.expand_path('../Runner.xcodeproj', __dir__), @tvos_root)
|
|
FileUtils.cp(File.expand_path('wire_mpv.rb', __dir__), File.join(@tvos_root, 'scripts'))
|
|
end
|
|
|
|
def teardown
|
|
FileUtils.remove_entry(@temporary_root)
|
|
end
|
|
|
|
def test_restores_missing_references_and_is_idempotent
|
|
edit_project do |_project, _runner, group|
|
|
AFFECTED_NAMES.each do |name|
|
|
group.files.find { |file| file.display_name == name }&.remove_from_project
|
|
end
|
|
end
|
|
|
|
run_wire_mpv
|
|
run_wire_mpv
|
|
assert_complete_source_graph
|
|
end
|
|
|
|
def test_restores_membership_when_references_remain
|
|
edit_project do |_project, runner, group|
|
|
affected = group.files.select { |file| AFFECTED_NAMES.include?(file.display_name) }
|
|
runner.source_build_phase.files.each do |build_file|
|
|
build_file.remove_from_project if affected.include?(build_file.file_ref)
|
|
end
|
|
end
|
|
|
|
run_wire_mpv
|
|
assert_complete_source_graph
|
|
end
|
|
|
|
def test_restores_missing_package_product_and_framework_edges
|
|
edit_project do |_project, runner, _group|
|
|
runner.package_product_dependencies
|
|
.select { |product| product.product_name == 'MPVKit' }
|
|
.each(&:remove_from_project)
|
|
end
|
|
|
|
run_wire_mpv
|
|
assert_complete_source_graph
|
|
end
|
|
|
|
def test_all_apple_targets_resolve_the_same_mpvkit_source
|
|
repository_root = File.expand_path('../..', __dir__)
|
|
%w[ios macos tvos].each do |platform|
|
|
resolved_path =
|
|
File.join(
|
|
repository_root,
|
|
platform,
|
|
'Runner.xcworkspace',
|
|
'xcshareddata',
|
|
'swiftpm',
|
|
'Package.resolved'
|
|
)
|
|
resolved = JSON.parse(File.read(resolved_path))
|
|
pin = resolved.fetch('pins').find { |candidate| candidate.fetch('identity') == 'mpvkit' }
|
|
refute_nil pin, "#{platform} must resolve MPVKit"
|
|
assert_equal MPVKIT_PIN['location'], pin['location'], "#{platform} MPVKit source"
|
|
assert_equal MPVKIT_PIN['revision'], pin.dig('state', 'revision'), "#{platform} MPVKit revision"
|
|
assert_equal MPVKIT_PIN['version'], pin.dig('state', 'version'), "#{platform} MPVKit version"
|
|
end
|
|
end
|
|
|
|
private
|
|
|
|
def project_path
|
|
File.join(@tvos_root, 'Runner.xcodeproj')
|
|
end
|
|
|
|
def edit_project
|
|
project = Xcodeproj::Project.open(project_path)
|
|
runner = project.targets.find { |target| target.name == 'Runner' }
|
|
group = project.main_group['Runner']['MpvPlayer']
|
|
yield project, runner, group
|
|
project.save
|
|
end
|
|
|
|
def run_wire_mpv
|
|
script = File.join(@tvos_root, 'scripts', 'wire_mpv.rb')
|
|
output, status = Open3.capture2e(RbConfig.ruby, script)
|
|
assert status.success?, output
|
|
end
|
|
|
|
def assert_complete_source_graph
|
|
project = Xcodeproj::Project.open(project_path)
|
|
runner = project.targets.find { |target| target.name == 'Runner' }
|
|
group = project.main_group['Runner']['MpvPlayer']
|
|
|
|
SOURCE_NAMES.each do |name|
|
|
references = group.files.select { |file| file.display_name == name }
|
|
assert_equal 1, references.count, "expected one reference for #{name}"
|
|
memberships = runner.source_build_phase.files_references.count { |file| file == references.first }
|
|
assert_equal 1, memberships, "expected one Runner source membership for #{name}"
|
|
end
|
|
|
|
products = runner.package_product_dependencies.select { |product| product.product_name == 'MPVKit' }
|
|
assert_equal 1, products.count, 'expected one MPVKit product dependency'
|
|
framework_links = runner.frameworks_build_phase.files.count { |file| file.product_ref == products.first }
|
|
assert_equal 1, framework_links, 'expected one MPVKit framework link'
|
|
end
|
|
end
|