1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33 package org.openimaj.video.xuggle;
34
35 import java.io.File;
36 import java.io.IOException;
37 import java.io.InputStream;
38 import java.net.MalformedURLException;
39 import java.net.URI;
40 import java.net.URISyntaxException;
41 import java.net.URL;
42 import java.util.concurrent.TimeUnit;
43
44 import org.apache.log4j.Logger;
45 import org.openimaj.audio.AudioFormat;
46 import org.openimaj.audio.AudioStream;
47 import org.openimaj.audio.SampleChunk;
48 import org.openimaj.audio.timecode.AudioTimecode;
49
50 import com.xuggle.mediatool.IMediaReader;
51 import com.xuggle.mediatool.MediaToolAdapter;
52 import com.xuggle.mediatool.ToolFactory;
53 import com.xuggle.mediatool.event.IAudioSamplesEvent;
54 import com.xuggle.xuggler.Global;
55 import com.xuggle.xuggler.IAudioSamples;
56 import com.xuggle.xuggler.ICodec;
57 import com.xuggle.xuggler.IContainer;
58 import com.xuggle.xuggler.IError;
59 import com.xuggle.xuggler.IStream;
60 import com.xuggle.xuggler.IStreamCoder;
61 import com.xuggle.xuggler.io.URLProtocolManager;
62
63
64
65
66
67
68
69
70
71 public class XuggleAudio extends AudioStream
72 {
73 static Logger logger = Logger.getLogger(XuggleAudio.class);
74
75 static {
76 URLProtocolManager.getManager().registerFactory("jar", new JarURLProtocolHandlerFactory());
77 }
78
79
80 private IMediaReader reader = null;
81
82
83 private int streamIndex = -1;
84
85
86 private SampleChunk currentSamples = null;
87
88
89 private boolean chunkAvailable = false;
90
91
92 private final AudioTimecode currentTimecode = new AudioTimecode(0);
93
94
95 private long length = -1;
96
97
98 private final String url;
99
100
101 private final boolean loop;
102
103
104
105
106
107 private boolean constructedFromStream = false;
108
109
110
111
112
113
114
115
116 protected class ChunkGetter extends MediaToolAdapter
117 {
118
119
120
121
122
123 @Override
124 public void onAudioSamples(final IAudioSamplesEvent event)
125 {
126
127 final IAudioSamples aSamples = event.getAudioSamples();
128 final byte[] rawBytes = aSamples.getData().
129 getByteArray(0, aSamples.getSize());
130 XuggleAudio.this.currentSamples.setSamples(rawBytes);
131
132
133
134
135
136 final long timestampMillisecs = TimeUnit.MILLISECONDS.convert(
137 event.getTimeStamp().longValue(), event.getTimeUnit());
138
139 XuggleAudio.this.currentTimecode.setTimecodeInMilliseconds(
140 timestampMillisecs);
141
142 XuggleAudio.this.currentSamples.setStartTimecode(
143 XuggleAudio.this.currentTimecode);
144
145 XuggleAudio.this.currentSamples.getFormat().setNumChannels(
146 XuggleAudio.this.getFormat().getNumChannels());
147
148 XuggleAudio.this.currentSamples.getFormat().setSigned(
149 XuggleAudio.this.getFormat().isSigned());
150
151 XuggleAudio.this.currentSamples.getFormat().setBigEndian(
152 XuggleAudio.this.getFormat().isBigEndian());
153
154 XuggleAudio.this.currentSamples.getFormat().setSampleRateKHz(
155 XuggleAudio.this.getFormat().getSampleRateKHz());
156
157 XuggleAudio.this.chunkAvailable = true;
158 }
159 }
160
161
162
163
164
165
166
167 public XuggleAudio(final File file)
168 {
169 this(file.toURI().toString(), false);
170 }
171
172
173
174
175
176
177
178
179
180 public XuggleAudio(final File file, final boolean loop)
181 {
182 this(file.toURI().toString(), loop);
183 }
184
185
186
187
188
189
190
191
192 public XuggleAudio(final URL u)
193 {
194 this(u.toString(), false);
195 }
196
197
198
199
200
201
202
203
204
205
206 public XuggleAudio(final URL u, final boolean loop)
207 {
208 this(u.toString(), loop);
209 }
210
211
212
213
214
215
216
217
218 public XuggleAudio(final String url)
219 {
220 this(url, false);
221 }
222
223
224
225
226
227
228
229
230
231
232
233
234
235 public XuggleAudio(final String u, final boolean loop)
236 {
237 this.url = u;
238 this.loop = loop;
239 this.create(null);
240 }
241
242
243
244
245
246
247
248 public XuggleAudio(final InputStream stream)
249 {
250 this.url = "stream://local";
251 this.loop = false;
252 this.constructedFromStream = true;
253 this.create(stream);
254 }
255
256
257
258
259
260
261
262 private void create(final InputStream stream)
263 {
264
265
266 if (this.reader != null && this.reader.isOpen())
267 {
268 this.reader.close();
269 this.reader = null;
270 }
271
272
273 IContainer container = null;
274 int openResult = 0;
275 try
276 {
277
278 container = IContainer.make();
279
280
281 if (stream != null)
282 {
283 openResult = container.open(stream, null, true, true);
284
285 if (openResult < 0)
286 logger.info("XuggleAudio could not open InputStream to audio.");
287 }
288
289 else
290 {
291 final URI uri = new URI(this.url);
292
293
294
295 openResult = container.open(uri.toString(),
296 IContainer.Type.READ, null, true, true);
297
298
299
300
301
302
303 if (openResult < 0)
304 {
305 logger.trace("URL " + this.url + " could not be opened by ffmpeg. " +
306 "Trying to open a stream to the URL instead.");
307 final InputStream is = uri.toURL().openStream();
308 openResult = container.open(is, null, true, true);
309
310 if (openResult < 0)
311 {
312 logger.error("Error opening container. Error " + openResult +
313 " (" + IError.errorNumberToType(openResult).toString() + ")");
314 return;
315 }
316 }
317 else
318 logger.info("Opened XuggleAudio stream ok: " + openResult);
319 }
320 } catch (final URISyntaxException e2)
321 {
322 e2.printStackTrace();
323 return;
324 } catch (final MalformedURLException e)
325 {
326 e.printStackTrace();
327 return;
328 } catch (final IOException e)
329 {
330 e.printStackTrace();
331 return;
332 }
333
334
335 this.reader = ToolFactory.makeReader(container);
336 this.reader.addListener(new ChunkGetter());
337 this.reader.setCloseOnEofOnly(!this.loop);
338
339
340 IStream s = null;
341 int i = 0;
342 while (i < container.getNumStreams())
343 {
344 s = container.getStream(i);
345 if (s != null &&
346 s.getStreamCoder().getCodecType() == ICodec.Type.CODEC_TYPE_AUDIO)
347 {
348
349
350 this.streamIndex = i;
351 break;
352 }
353 i++;
354 }
355 logger.info("Using audio stream " + this.streamIndex);
356
357 if (container.getDuration() == Global.NO_PTS)
358 this.length = -1;
359 else
360 this.length = (long) (s.getDuration() *
361 s.getTimeBase().getDouble() * 1000d);
362
363
364 final IStreamCoder aAudioCoder = container.
365 getStream(this.streamIndex).getStreamCoder();
366
367 logger.info("Using stream code: " + aAudioCoder);
368
369
370
371 final AudioFormat af = new AudioFormat(
372 (int) IAudioSamples.findSampleBitDepth(aAudioCoder.getSampleFormat()),
373 aAudioCoder.getSampleRate() / 1000d,
374 aAudioCoder.getChannels());
375 af.setSigned(true);
376 af.setBigEndian(false);
377 super.format = af;
378
379 logger.info("XuggleAudio using audio format: " + af);
380
381 this.currentSamples = new SampleChunk(af.clone());
382 }
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404 @Override
405 public SampleChunk nextSampleChunk()
406 {
407 try
408 {
409 IError e = null;
410 while ((e = this.reader.readPacket()) == null && !this.chunkAvailable)
411 ;
412
413 if (!this.chunkAvailable) {
414 this.reader.close();
415 this.reader = null;
416 return null;
417 }
418
419 if (e != null)
420 {
421 this.reader.close();
422 this.reader = null;
423
424
425
426
427
428
429
430
431
432
433
434 return null;
435 }
436
437 this.chunkAvailable = false;
438 return this.currentSamples;
439 } catch (final Exception e) {
440 }
441
442 return null;
443 }
444
445
446
447
448
449
450 @Override
451 public void reset()
452 {
453 if (this.constructedFromStream)
454 {
455 logger.info("Cannot reset a stream of audio.");
456 return;
457 }
458
459 if (this.reader == null || this.reader.getContainer() == null)
460 this.create(null);
461 else
462 this.seek(0);
463 }
464
465
466
467
468
469
470 @Override
471 public long getLength()
472 {
473 return this.length;
474 }
475
476
477
478
479
480
481 @Override
482 public void seek(final long timestamp)
483 {
484 if (this.constructedFromStream)
485 {
486 logger.info("Cannot seek within a stream of audio.");
487 return;
488 }
489
490 if (this.reader == null || this.reader.getContainer() == null)
491 this.create(null);
492
493
494 final double timebase = this.reader.getContainer().getStream(
495 this.streamIndex).getTimeBase().getDouble();
496 final long position = (long) (timestamp / timebase);
497
498 final long min = Math.max(0, position - 100);
499 final long max = position;
500
501
502
503
504
505
506 final int i = this.reader.getContainer().seekKeyFrame(this.streamIndex,
507 min, position, max, 0);
508
509
510 if (i < 0)
511 logger.error("Audio seek error (" + i + "): " + IError.errorNumberToType(i));
512 else
513 this.nextSampleChunk();
514 }
515
516
517
518
519 public synchronized void close()
520 {
521 if (this.reader != null)
522 {
523 synchronized (this.reader)
524 {
525 if (this.reader.isOpen())
526 {
527 this.reader.close();
528 this.reader = null;
529 }
530 }
531 }
532 }
533 }