Skip to content

Commit 1debfd0

Browse files
[NOVA-2683] Exclude unknown audio codec (#34)
1 parent e36e7aa commit 1debfd0

File tree

5 files changed

+188
-5
lines changed

5 files changed

+188
-5
lines changed

lib/ffmpeg/movie.rb

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,8 @@ def initialize(paths, analyzeduration = 15000000, probesize=15000000 )
6262
@duration = 0
6363
else
6464
video_streams = metadata[:streams].select { |stream| stream.key?(:codec_type) and stream[:codec_type] === 'video' }
65-
audio_streams = metadata[:streams].select { |stream| stream.key?(:codec_type) and stream[:codec_type] === 'audio' }
65+
audio_streams = metadata[:streams].select { |stream| stream.key?(:codec_type) and stream[:codec_type] === 'audio' and stream[:codec_name] and stream[:codec_name] != 'none' }
66+
6667
data_streams = metadata[:streams].select { |stream| stream.key?(:codec_type) and stream[:codec_type] === 'data' }
6768

6869
@container = metadata[:format][:format_name]

lib/ffmpeg/transcoder.rb

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -160,8 +160,12 @@ def pre_encode_if_necessary
160160

161161
def determine_audio_for_pre_encode(path)
162162
local_movie = Movie.new(path)
163-
# If there's a local audio stream, use that
164-
return '-map "0:a"' if local_movie.audio_streams.any?
163+
# If there's a local audio stream, use that. Map only decodable streams explicitly -
164+
# using -map "0:a" would include APAC (Apple Positional Audio Codec) from iPhone
165+
# spatial audio, which FFmpeg cannot decode. audio_streams already excludes these.
166+
if local_movie.audio_streams.any?
167+
return local_movie.audio_streams.map { |s| "-map \"0:#{s[:index]}\"" }.join(' ')
168+
end
165169
# Otherwise, use a silent audio source
166170
# | aevalsrc=0 will generate a silent audio source
167171
# | -shortest will make sure that the output is the duration of the shortest input (meaning the real source input)

spec/ffmpeg/movie_spec.rb

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -249,6 +249,35 @@ module FFMPEG # rubocop:todo Metrics/ModuleLength
249249
end
250250
end
251251

252+
context "given an iPhone recording with an APAC (Apple Positional Audio Codec) stream" do
253+
before(:each) do
254+
fake_stdout = File.read("#{fixture_path}/outputs/file_with_apac_audio.txt")
255+
spawn_double = double(:out => fake_stdout, :err => '')
256+
expect(POSIX::Spawn::Child).to receive(:new).and_return(spawn_double)
257+
@movie = Movie.new(__FILE__)
258+
end
259+
260+
it "should be valid" do
261+
expect(@movie).to be_valid
262+
end
263+
264+
it "should only include the decodable AAC audio stream" do
265+
expect(@movie.audio_streams.length).to eq(1)
266+
expect(@movie.audio_streams[0][:codec_name]).to eq('aac')
267+
end
268+
269+
it "should not include the APAC stream in audio_streams" do
270+
apac_stream = @movie.audio_streams.find { |s| s[:codec_name] == 'none' }
271+
expect(apac_stream).to be_nil
272+
end
273+
274+
it "should set audio properties from the AAC stream" do
275+
expect(@movie.audio_codec).to eq('aac')
276+
expect(@movie.audio_channels).to eq(2)
277+
expect(@movie.audio_sample_rate).to eq(48000)
278+
end
279+
end
280+
252281
context "given an awesome movie file" do
253282
before(:all) do
254283
@movie = Movie.new("#{fixture_path}/movies/awesome movie.mov")

spec/ffmpeg/transcoder_spec.rb

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -348,7 +348,8 @@ module FFMPEG
348348
transcoder = Transcoder.new(movie_with_multiple_dimension_inputs, output_path, EncodingOptions.new)
349349

350350
expect(Open3).to receive(:popen3).exactly(2).times # prevent ffprobe calls from being evaluated (check_frame_resolutions)
351-
expect(Open3).to receive(:popen3).twice.with match(/.*\[0\:v\]scale\=854\:480.*pad\=854\:480\:\(ow\-iw\)\/2\:\(oh\-ih\)\/2\:color\=black.*\-map \"0\:a\".*/)
351+
# Maps decodable audio streams by index (excludes APAC); awesome_widescreen has 1 audio stream
352+
expect(Open3).to receive(:popen3).twice.with match(/.*\[0\:v\]scale\=854\:480.*pad\=854\:480\:\(ow\-iw\)\/2\:\(oh\-ih\)\/2\:color\=black.*\-map \"0\:\d+\".*/)
352353

353354
transcoder.send(:pre_encode_if_necessary)
354355
end
@@ -360,7 +361,8 @@ module FFMPEG
360361
# Match silent audio fill
361362
expect(Open3).to receive(:popen3).once.with match(/.*\[0\:v\]scale\=960\:540.*pad\=960\:540\:\(ow\-iw\)\/2\:\(oh\-ih\)\/2\:color\=black.*\-map \"\[a\]\".*/)
362363
# Retain "real" audio
363-
expect(Open3).to receive(:popen3).once.with match(/.*\[0\:v\]scale\=960\:540.*pad\=960\:540\:\(ow\-iw\)\/2\:\(oh\-ih\)\/2\:color\=black.*\-map \"0\:a\".*/)
364+
# Retain "real" audio - maps decodable audio streams by index
365+
expect(Open3).to receive(:popen3).once.with match(/.*\[0\:v\]scale\=960\:540.*pad\=960\:540\:\(ow\-iw\)\/2\:\(oh\-ih\)\/2\:color\=black.*\-map \"0\:\d+\".*/)
364366

365367
transcoder.send(:pre_encode_if_necessary)
366368
end
Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
{
2+
"streams": [
3+
{
4+
"index": 0,
5+
"codec_name": "hevc",
6+
"codec_long_name": "H.265 / HEVC (High Efficiency Video Coding)",
7+
"profile": "Main 10",
8+
"codec_type": "video",
9+
"codec_tag_string": "hvc1",
10+
"codec_tag": "0x31637668",
11+
"width": 1920,
12+
"height": 1080,
13+
"has_b_frames": 0,
14+
"pix_fmt": "yuv420p10le",
15+
"r_frame_rate": "30/1",
16+
"avg_frame_rate": "30/1",
17+
"time_base": "1/600",
18+
"start_pts": 0,
19+
"start_time": "0.000000",
20+
"duration_ts": 2502,
21+
"duration": "4.170000",
22+
"bit_rate": "5943000",
23+
"nb_frames": "125",
24+
"disposition": {
25+
"default": 1,
26+
"dub": 0,
27+
"original": 0,
28+
"comment": 0,
29+
"lyrics": 0,
30+
"karaoke": 0,
31+
"forced": 0,
32+
"hearing_impaired": 0,
33+
"visual_impaired": 0,
34+
"clean_effects": 0,
35+
"attached_pic": 0
36+
},
37+
"tags": {
38+
"creation_time": "2025-12-04T13:49:37.000000Z",
39+
"language": "und",
40+
"handler_name": "Core Media Video",
41+
"vendor_id": "[0][0][0][0]",
42+
"encoder": "HEVC"
43+
}
44+
},
45+
{
46+
"index": 1,
47+
"codec_name": "aac",
48+
"codec_long_name": "AAC (Advanced Audio Coding)",
49+
"profile": "LC",
50+
"codec_type": "audio",
51+
"codec_tag_string": "mp4a",
52+
"codec_tag": "0x6134706d",
53+
"sample_rate": "48000",
54+
"channels": 2,
55+
"channel_layout": "stereo",
56+
"bits_per_sample": 0,
57+
"r_frame_rate": "0/0",
58+
"avg_frame_rate": "0/0",
59+
"time_base": "1/48000",
60+
"start_pts": 0,
61+
"start_time": "0.000000",
62+
"duration_ts": 200704,
63+
"duration": "4.181333",
64+
"bit_rate": "200000",
65+
"nb_frames": "196",
66+
"disposition": {
67+
"default": 1,
68+
"dub": 0,
69+
"original": 0,
70+
"comment": 0,
71+
"lyrics": 0,
72+
"karaoke": 0,
73+
"forced": 0,
74+
"hearing_impaired": 0,
75+
"visual_impaired": 0,
76+
"clean_effects": 0,
77+
"attached_pic": 0
78+
},
79+
"tags": {
80+
"creation_time": "2025-12-04T13:49:37.000000Z",
81+
"language": "und",
82+
"handler_name": "Core Media Audio",
83+
"vendor_id": "[0][0][0][0]"
84+
}
85+
},
86+
{
87+
"index": 2,
88+
"codec_name": "none",
89+
"codec_long_name": "unknown",
90+
"codec_type": "audio",
91+
"codec_tag_string": "apac",
92+
"codec_tag": "0x63617061",
93+
"sample_rate": "48000",
94+
"channels": 4,
95+
"channel_layout": "4.0",
96+
"bits_per_sample": 0,
97+
"r_frame_rate": "0/0",
98+
"avg_frame_rate": "0/0",
99+
"time_base": "1/48000",
100+
"start_pts": 0,
101+
"start_time": "0.000000",
102+
"duration_ts": 200704,
103+
"duration": "4.181333",
104+
"bit_rate": "394000",
105+
"nb_frames": "196",
106+
"disposition": {
107+
"default": 0,
108+
"dub": 0,
109+
"original": 0,
110+
"comment": 0,
111+
"lyrics": 0,
112+
"karaoke": 0,
113+
"forced": 0,
114+
"hearing_impaired": 0,
115+
"visual_impaired": 0,
116+
"clean_effects": 0,
117+
"attached_pic": 0
118+
},
119+
"tags": {
120+
"creation_time": "2025-12-04T13:49:37.000000Z",
121+
"language": "und",
122+
"handler_name": "Core Media Audio",
123+
"vendor_id": "[0][0][0][0]"
124+
}
125+
}
126+
],
127+
"format": {
128+
"filename": "iphone_with_apac.mp4",
129+
"nb_streams": 3,
130+
"nb_programs": 0,
131+
"format_name": "mov,mp4,m4a,3gp,3g2,mj2",
132+
"format_long_name": "QuickTime / MOV",
133+
"start_time": "0.000000",
134+
"duration": "4.170000",
135+
"size": "3441234",
136+
"bit_rate": "6607000",
137+
"probe_score": 100,
138+
"tags": {
139+
"major_brand": "qt ",
140+
"minor_version": "0",
141+
"compatible_brands": "qt ",
142+
"creation_time": "2025-12-04T13:49:37.000000Z",
143+
"com.apple.quicktime.make": "Apple",
144+
"com.apple.quicktime.model": "iPhone 16"
145+
}
146+
}
147+
}

0 commit comments

Comments
 (0)