|
1 /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ |
|
2 /* vim:set ts=2 sw=2 sts=2 et cindent: */ |
|
3 /* This Source Code Form is subject to the terms of the Mozilla Public |
|
4 * License, v. 2.0. If a copy of the MPL was not distributed with this |
|
5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ |
|
6 #include "nsError.h" |
|
7 #include "MediaDecoderStateMachine.h" |
|
8 #include "AbstractMediaDecoder.h" |
|
9 #include "MediaResource.h" |
|
10 #include "WebMReader.h" |
|
11 #include "WebMBufferedParser.h" |
|
12 #include "mozilla/dom/TimeRanges.h" |
|
13 #include "VorbisUtils.h" |
|
14 #include "gfx2DGlue.h" |
|
15 |
|
16 #include <algorithm> |
|
17 |
|
18 #define VPX_DONT_DEFINE_STDINT_TYPES |
|
19 #include "vpx/vp8dx.h" |
|
20 #include "vpx/vpx_decoder.h" |
|
21 |
|
22 #include "OggReader.h" |
|
23 |
|
24 using mozilla::NesteggPacketHolder; |
|
25 |
|
26 template <> |
|
27 class nsAutoRefTraits<NesteggPacketHolder> : public nsPointerRefTraits<NesteggPacketHolder> |
|
28 { |
|
29 public: |
|
30 static void Release(NesteggPacketHolder* aHolder) { delete aHolder; } |
|
31 }; |
|
32 |
|
33 namespace mozilla { |
|
34 |
|
35 using namespace gfx; |
|
36 using namespace layers; |
|
37 |
|
38 // Un-comment to enable logging of seek bisections. |
|
39 //#define SEEK_LOGGING |
|
40 |
|
41 #ifdef PR_LOGGING |
|
42 extern PRLogModuleInfo* gMediaDecoderLog; |
|
43 PRLogModuleInfo* gNesteggLog; |
|
44 #define LOG(type, msg) PR_LOG(gMediaDecoderLog, type, msg) |
|
45 #ifdef SEEK_LOGGING |
|
46 #define SEEK_LOG(type, msg) PR_LOG(gMediaDecoderLog, type, msg) |
|
47 #else |
|
48 #define SEEK_LOG(type, msg) |
|
49 #endif |
|
50 #else |
|
51 #define LOG(type, msg) |
|
52 #define SEEK_LOG(type, msg) |
|
53 #endif |
|
54 |
|
55 static const unsigned NS_PER_USEC = 1000; |
|
56 static const double NS_PER_S = 1e9; |
|
57 |
|
58 // Functions for reading and seeking using MediaResource required for |
|
59 // nestegg_io. The 'user data' passed to these functions is the |
|
60 // decoder from which the media resource is obtained. |
|
61 static int webm_read(void *aBuffer, size_t aLength, void *aUserData) |
|
62 { |
|
63 NS_ASSERTION(aUserData, "aUserData must point to a valid AbstractMediaDecoder"); |
|
64 AbstractMediaDecoder* decoder = reinterpret_cast<AbstractMediaDecoder*>(aUserData); |
|
65 MediaResource* resource = decoder->GetResource(); |
|
66 NS_ASSERTION(resource, "Decoder has no media resource"); |
|
67 |
|
68 nsresult rv = NS_OK; |
|
69 bool eof = false; |
|
70 |
|
71 char *p = static_cast<char *>(aBuffer); |
|
72 while (NS_SUCCEEDED(rv) && aLength > 0) { |
|
73 uint32_t bytes = 0; |
|
74 rv = resource->Read(p, aLength, &bytes); |
|
75 if (bytes == 0) { |
|
76 eof = true; |
|
77 break; |
|
78 } |
|
79 aLength -= bytes; |
|
80 p += bytes; |
|
81 } |
|
82 |
|
83 return NS_FAILED(rv) ? -1 : eof ? 0 : 1; |
|
84 } |
|
85 |
|
86 static int webm_seek(int64_t aOffset, int aWhence, void *aUserData) |
|
87 { |
|
88 NS_ASSERTION(aUserData, "aUserData must point to a valid AbstractMediaDecoder"); |
|
89 AbstractMediaDecoder* decoder = reinterpret_cast<AbstractMediaDecoder*>(aUserData); |
|
90 MediaResource* resource = decoder->GetResource(); |
|
91 NS_ASSERTION(resource, "Decoder has no media resource"); |
|
92 nsresult rv = resource->Seek(aWhence, aOffset); |
|
93 return NS_SUCCEEDED(rv) ? 0 : -1; |
|
94 } |
|
95 |
|
96 static int64_t webm_tell(void *aUserData) |
|
97 { |
|
98 NS_ASSERTION(aUserData, "aUserData must point to a valid AbstractMediaDecoder"); |
|
99 AbstractMediaDecoder* decoder = reinterpret_cast<AbstractMediaDecoder*>(aUserData); |
|
100 MediaResource* resource = decoder->GetResource(); |
|
101 NS_ASSERTION(resource, "Decoder has no media resource"); |
|
102 return resource->Tell(); |
|
103 } |
|
104 |
|
105 static void webm_log(nestegg * context, |
|
106 unsigned int severity, |
|
107 char const * format, ...) |
|
108 { |
|
109 #ifdef PR_LOGGING |
|
110 va_list args; |
|
111 char msg[256]; |
|
112 const char * sevStr; |
|
113 |
|
114 switch(severity) { |
|
115 case NESTEGG_LOG_DEBUG: |
|
116 sevStr = "DBG"; |
|
117 break; |
|
118 case NESTEGG_LOG_INFO: |
|
119 sevStr = "INF"; |
|
120 break; |
|
121 case NESTEGG_LOG_WARNING: |
|
122 sevStr = "WRN"; |
|
123 break; |
|
124 case NESTEGG_LOG_ERROR: |
|
125 sevStr = "ERR"; |
|
126 break; |
|
127 case NESTEGG_LOG_CRITICAL: |
|
128 sevStr = "CRT"; |
|
129 break; |
|
130 default: |
|
131 sevStr = "UNK"; |
|
132 break; |
|
133 } |
|
134 |
|
135 va_start(args, format); |
|
136 |
|
137 PR_snprintf(msg, sizeof(msg), "%p [Nestegg-%s] ", context, sevStr); |
|
138 PR_vsnprintf(msg+strlen(msg), sizeof(msg)-strlen(msg), format, args); |
|
139 PR_LOG(gNesteggLog, PR_LOG_DEBUG, (msg)); |
|
140 |
|
141 va_end(args); |
|
142 #endif |
|
143 } |
|
144 |
|
145 WebMReader::WebMReader(AbstractMediaDecoder* aDecoder) |
|
146 : MediaDecoderReader(aDecoder), |
|
147 mContext(nullptr), |
|
148 mPacketCount(0), |
|
149 mChannels(0), |
|
150 #ifdef MOZ_OPUS |
|
151 mOpusParser(nullptr), |
|
152 mOpusDecoder(nullptr), |
|
153 mSkip(0), |
|
154 mSeekPreroll(0), |
|
155 #endif |
|
156 mVideoTrack(0), |
|
157 mAudioTrack(0), |
|
158 mAudioStartUsec(-1), |
|
159 mAudioFrames(0), |
|
160 mAudioCodec(-1), |
|
161 mVideoCodec(-1), |
|
162 mHasVideo(false), |
|
163 mHasAudio(false) |
|
164 { |
|
165 MOZ_COUNT_CTOR(WebMReader); |
|
166 #ifdef PR_LOGGING |
|
167 if (!gNesteggLog) { |
|
168 gNesteggLog = PR_NewLogModule("Nestegg"); |
|
169 } |
|
170 #endif |
|
171 // Zero these member vars to avoid crashes in VP8 destroy and Vorbis clear |
|
172 // functions when destructor is called before |Init|. |
|
173 memset(&mVPX, 0, sizeof(vpx_codec_ctx_t)); |
|
174 memset(&mVorbisBlock, 0, sizeof(vorbis_block)); |
|
175 memset(&mVorbisDsp, 0, sizeof(vorbis_dsp_state)); |
|
176 memset(&mVorbisInfo, 0, sizeof(vorbis_info)); |
|
177 memset(&mVorbisComment, 0, sizeof(vorbis_comment)); |
|
178 } |
|
179 |
|
180 WebMReader::~WebMReader() |
|
181 { |
|
182 Cleanup(); |
|
183 |
|
184 mVideoPackets.Reset(); |
|
185 mAudioPackets.Reset(); |
|
186 |
|
187 vpx_codec_destroy(&mVPX); |
|
188 |
|
189 vorbis_block_clear(&mVorbisBlock); |
|
190 vorbis_dsp_clear(&mVorbisDsp); |
|
191 vorbis_info_clear(&mVorbisInfo); |
|
192 vorbis_comment_clear(&mVorbisComment); |
|
193 |
|
194 if (mOpusDecoder) { |
|
195 opus_multistream_decoder_destroy(mOpusDecoder); |
|
196 mOpusDecoder = nullptr; |
|
197 } |
|
198 |
|
199 MOZ_COUNT_DTOR(WebMReader); |
|
200 } |
|
201 |
|
202 nsresult WebMReader::Init(MediaDecoderReader* aCloneDonor) |
|
203 { |
|
204 |
|
205 vorbis_info_init(&mVorbisInfo); |
|
206 vorbis_comment_init(&mVorbisComment); |
|
207 memset(&mVorbisDsp, 0, sizeof(vorbis_dsp_state)); |
|
208 memset(&mVorbisBlock, 0, sizeof(vorbis_block)); |
|
209 |
|
210 if (aCloneDonor) { |
|
211 mBufferedState = static_cast<WebMReader*>(aCloneDonor)->mBufferedState; |
|
212 } else { |
|
213 mBufferedState = new WebMBufferedState; |
|
214 } |
|
215 |
|
216 return NS_OK; |
|
217 } |
|
218 |
|
219 nsresult WebMReader::ResetDecode() |
|
220 { |
|
221 mAudioFrames = 0; |
|
222 mAudioStartUsec = -1; |
|
223 nsresult res = NS_OK; |
|
224 if (NS_FAILED(MediaDecoderReader::ResetDecode())) { |
|
225 res = NS_ERROR_FAILURE; |
|
226 } |
|
227 |
|
228 if (mAudioCodec == NESTEGG_CODEC_VORBIS) { |
|
229 // Ignore failed results from vorbis_synthesis_restart. They |
|
230 // aren't fatal and it fails when ResetDecode is called at a |
|
231 // time when no vorbis data has been read. |
|
232 vorbis_synthesis_restart(&mVorbisDsp); |
|
233 #ifdef MOZ_OPUS |
|
234 } else if (mAudioCodec == NESTEGG_CODEC_OPUS) { |
|
235 if (mOpusDecoder) { |
|
236 // Reset the decoder. |
|
237 opus_multistream_decoder_ctl(mOpusDecoder, OPUS_RESET_STATE); |
|
238 mSkip = mOpusParser->mPreSkip; |
|
239 } |
|
240 #endif |
|
241 } |
|
242 |
|
243 mVideoPackets.Reset(); |
|
244 mAudioPackets.Reset(); |
|
245 |
|
246 return res; |
|
247 } |
|
248 |
|
249 void WebMReader::Cleanup() |
|
250 { |
|
251 if (mContext) { |
|
252 nestegg_destroy(mContext); |
|
253 mContext = nullptr; |
|
254 } |
|
255 } |
|
256 |
|
257 nsresult WebMReader::ReadMetadata(MediaInfo* aInfo, |
|
258 MetadataTags** aTags) |
|
259 { |
|
260 NS_ASSERTION(mDecoder->OnDecodeThread(), "Should be on decode thread."); |
|
261 |
|
262 nestegg_io io; |
|
263 io.read = webm_read; |
|
264 io.seek = webm_seek; |
|
265 io.tell = webm_tell; |
|
266 io.userdata = mDecoder; |
|
267 int64_t maxOffset = -1; |
|
268 int r = nestegg_init(&mContext, io, &webm_log, maxOffset); |
|
269 if (r == -1) { |
|
270 return NS_ERROR_FAILURE; |
|
271 } |
|
272 |
|
273 uint64_t duration = 0; |
|
274 r = nestegg_duration(mContext, &duration); |
|
275 if (r == 0) { |
|
276 ReentrantMonitorAutoEnter mon(mDecoder->GetReentrantMonitor()); |
|
277 mDecoder->SetMediaDuration(duration / NS_PER_USEC); |
|
278 } |
|
279 |
|
280 unsigned int ntracks = 0; |
|
281 r = nestegg_track_count(mContext, &ntracks); |
|
282 if (r == -1) { |
|
283 Cleanup(); |
|
284 return NS_ERROR_FAILURE; |
|
285 } |
|
286 |
|
287 for (uint32_t track = 0; track < ntracks; ++track) { |
|
288 int id = nestegg_track_codec_id(mContext, track); |
|
289 if (id == -1) { |
|
290 Cleanup(); |
|
291 return NS_ERROR_FAILURE; |
|
292 } |
|
293 int type = nestegg_track_type(mContext, track); |
|
294 if (!mHasVideo && type == NESTEGG_TRACK_VIDEO) { |
|
295 nestegg_video_params params; |
|
296 r = nestegg_track_video_params(mContext, track, ¶ms); |
|
297 if (r == -1) { |
|
298 Cleanup(); |
|
299 return NS_ERROR_FAILURE; |
|
300 } |
|
301 |
|
302 vpx_codec_iface_t* dx = nullptr; |
|
303 mVideoCodec = nestegg_track_codec_id(mContext, track); |
|
304 if (mVideoCodec == NESTEGG_CODEC_VP8) { |
|
305 dx = vpx_codec_vp8_dx(); |
|
306 } else if (mVideoCodec == NESTEGG_CODEC_VP9) { |
|
307 dx = vpx_codec_vp9_dx(); |
|
308 } |
|
309 if (!dx || vpx_codec_dec_init(&mVPX, dx, nullptr, 0)) { |
|
310 Cleanup(); |
|
311 return NS_ERROR_FAILURE; |
|
312 } |
|
313 |
|
314 // Picture region, taking into account cropping, before scaling |
|
315 // to the display size. |
|
316 nsIntRect pictureRect(params.crop_left, |
|
317 params.crop_top, |
|
318 params.width - (params.crop_right + params.crop_left), |
|
319 params.height - (params.crop_bottom + params.crop_top)); |
|
320 |
|
321 // If the cropping data appears invalid then use the frame data |
|
322 if (pictureRect.width <= 0 || |
|
323 pictureRect.height <= 0 || |
|
324 pictureRect.x < 0 || |
|
325 pictureRect.y < 0) |
|
326 { |
|
327 pictureRect.x = 0; |
|
328 pictureRect.y = 0; |
|
329 pictureRect.width = params.width; |
|
330 pictureRect.height = params.height; |
|
331 } |
|
332 |
|
333 // Validate the container-reported frame and pictureRect sizes. This ensures |
|
334 // that our video frame creation code doesn't overflow. |
|
335 nsIntSize displaySize(params.display_width, params.display_height); |
|
336 nsIntSize frameSize(params.width, params.height); |
|
337 if (!IsValidVideoRegion(frameSize, pictureRect, displaySize)) { |
|
338 // Video track's frame sizes will overflow. Ignore the video track. |
|
339 continue; |
|
340 } |
|
341 |
|
342 mVideoTrack = track; |
|
343 mHasVideo = true; |
|
344 mInfo.mVideo.mHasVideo = true; |
|
345 |
|
346 mInfo.mVideo.mDisplay = displaySize; |
|
347 mPicture = pictureRect; |
|
348 mInitialFrame = frameSize; |
|
349 |
|
350 switch (params.stereo_mode) { |
|
351 case NESTEGG_VIDEO_MONO: |
|
352 mInfo.mVideo.mStereoMode = StereoMode::MONO; |
|
353 break; |
|
354 case NESTEGG_VIDEO_STEREO_LEFT_RIGHT: |
|
355 mInfo.mVideo.mStereoMode = StereoMode::LEFT_RIGHT; |
|
356 break; |
|
357 case NESTEGG_VIDEO_STEREO_BOTTOM_TOP: |
|
358 mInfo.mVideo.mStereoMode = StereoMode::BOTTOM_TOP; |
|
359 break; |
|
360 case NESTEGG_VIDEO_STEREO_TOP_BOTTOM: |
|
361 mInfo.mVideo.mStereoMode = StereoMode::TOP_BOTTOM; |
|
362 break; |
|
363 case NESTEGG_VIDEO_STEREO_RIGHT_LEFT: |
|
364 mInfo.mVideo.mStereoMode = StereoMode::RIGHT_LEFT; |
|
365 break; |
|
366 } |
|
367 } |
|
368 else if (!mHasAudio && type == NESTEGG_TRACK_AUDIO) { |
|
369 nestegg_audio_params params; |
|
370 r = nestegg_track_audio_params(mContext, track, ¶ms); |
|
371 if (r == -1) { |
|
372 Cleanup(); |
|
373 return NS_ERROR_FAILURE; |
|
374 } |
|
375 |
|
376 mAudioTrack = track; |
|
377 mHasAudio = true; |
|
378 mInfo.mAudio.mHasAudio = true; |
|
379 mAudioCodec = nestegg_track_codec_id(mContext, track); |
|
380 mCodecDelay = params.codec_delay / NS_PER_USEC; |
|
381 |
|
382 if (mAudioCodec == NESTEGG_CODEC_VORBIS) { |
|
383 // Get the Vorbis header data |
|
384 unsigned int nheaders = 0; |
|
385 r = nestegg_track_codec_data_count(mContext, track, &nheaders); |
|
386 if (r == -1 || nheaders != 3) { |
|
387 Cleanup(); |
|
388 return NS_ERROR_FAILURE; |
|
389 } |
|
390 |
|
391 for (uint32_t header = 0; header < nheaders; ++header) { |
|
392 unsigned char* data = 0; |
|
393 size_t length = 0; |
|
394 |
|
395 r = nestegg_track_codec_data(mContext, track, header, &data, &length); |
|
396 if (r == -1) { |
|
397 Cleanup(); |
|
398 return NS_ERROR_FAILURE; |
|
399 } |
|
400 ogg_packet opacket = InitOggPacket(data, length, header == 0, false, 0); |
|
401 |
|
402 r = vorbis_synthesis_headerin(&mVorbisInfo, |
|
403 &mVorbisComment, |
|
404 &opacket); |
|
405 if (r != 0) { |
|
406 Cleanup(); |
|
407 return NS_ERROR_FAILURE; |
|
408 } |
|
409 } |
|
410 |
|
411 r = vorbis_synthesis_init(&mVorbisDsp, &mVorbisInfo); |
|
412 if (r != 0) { |
|
413 Cleanup(); |
|
414 return NS_ERROR_FAILURE; |
|
415 } |
|
416 |
|
417 r = vorbis_block_init(&mVorbisDsp, &mVorbisBlock); |
|
418 if (r != 0) { |
|
419 Cleanup(); |
|
420 return NS_ERROR_FAILURE; |
|
421 } |
|
422 |
|
423 mInfo.mAudio.mRate = mVorbisDsp.vi->rate; |
|
424 mInfo.mAudio.mChannels = mVorbisDsp.vi->channels; |
|
425 mChannels = mInfo.mAudio.mChannels; |
|
426 #ifdef MOZ_OPUS |
|
427 } else if (mAudioCodec == NESTEGG_CODEC_OPUS) { |
|
428 unsigned char* data = 0; |
|
429 size_t length = 0; |
|
430 r = nestegg_track_codec_data(mContext, track, 0, &data, &length); |
|
431 if (r == -1) { |
|
432 Cleanup(); |
|
433 return NS_ERROR_FAILURE; |
|
434 } |
|
435 |
|
436 mOpusParser = new OpusParser; |
|
437 if (!mOpusParser->DecodeHeader(data, length)) { |
|
438 Cleanup(); |
|
439 return NS_ERROR_FAILURE; |
|
440 } |
|
441 |
|
442 if (!InitOpusDecoder()) { |
|
443 Cleanup(); |
|
444 return NS_ERROR_FAILURE; |
|
445 } |
|
446 |
|
447 if (static_cast<int64_t>(mCodecDelay) != FramesToUsecs(mOpusParser->mPreSkip, mOpusParser->mRate).value()) { |
|
448 LOG(PR_LOG_WARNING, |
|
449 ("Invalid Opus header: CodecDelay and pre-skip do not match!\n")); |
|
450 Cleanup(); |
|
451 return NS_ERROR_FAILURE; |
|
452 } |
|
453 |
|
454 mInfo.mAudio.mRate = mOpusParser->mRate; |
|
455 |
|
456 mInfo.mAudio.mChannels = mOpusParser->mChannels; |
|
457 mChannels = mInfo.mAudio.mChannels; |
|
458 mSeekPreroll = params.seek_preroll; |
|
459 #endif |
|
460 } else { |
|
461 Cleanup(); |
|
462 return NS_ERROR_FAILURE; |
|
463 } |
|
464 } |
|
465 } |
|
466 |
|
467 // We can't seek in buffered regions if we have no cues. |
|
468 mDecoder->SetMediaSeekable(nestegg_has_cues(mContext) == 1); |
|
469 |
|
470 *aInfo = mInfo; |
|
471 |
|
472 *aTags = nullptr; |
|
473 |
|
474 return NS_OK; |
|
475 } |
|
476 |
|
477 #ifdef MOZ_OPUS |
|
478 bool WebMReader::InitOpusDecoder() |
|
479 { |
|
480 int r; |
|
481 |
|
482 NS_ASSERTION(mOpusDecoder == nullptr, "leaking OpusDecoder"); |
|
483 |
|
484 mOpusDecoder = opus_multistream_decoder_create(mOpusParser->mRate, |
|
485 mOpusParser->mChannels, |
|
486 mOpusParser->mStreams, |
|
487 mOpusParser->mCoupledStreams, |
|
488 mOpusParser->mMappingTable, |
|
489 &r); |
|
490 mSkip = mOpusParser->mPreSkip; |
|
491 |
|
492 return r == OPUS_OK; |
|
493 } |
|
494 #endif |
|
495 |
|
496 ogg_packet WebMReader::InitOggPacket(unsigned char* aData, |
|
497 size_t aLength, |
|
498 bool aBOS, |
|
499 bool aEOS, |
|
500 int64_t aGranulepos) |
|
501 { |
|
502 ogg_packet packet; |
|
503 packet.packet = aData; |
|
504 packet.bytes = aLength; |
|
505 packet.b_o_s = aBOS; |
|
506 packet.e_o_s = aEOS; |
|
507 packet.granulepos = aGranulepos; |
|
508 packet.packetno = mPacketCount++; |
|
509 return packet; |
|
510 } |
|
511 |
|
512 bool WebMReader::DecodeAudioPacket(nestegg_packet* aPacket, int64_t aOffset) |
|
513 { |
|
514 NS_ASSERTION(mDecoder->OnDecodeThread(), "Should be on decode thread."); |
|
515 |
|
516 int r = 0; |
|
517 unsigned int count = 0; |
|
518 r = nestegg_packet_count(aPacket, &count); |
|
519 if (r == -1) { |
|
520 return false; |
|
521 } |
|
522 |
|
523 uint64_t tstamp = 0; |
|
524 r = nestegg_packet_tstamp(aPacket, &tstamp); |
|
525 if (r == -1) { |
|
526 return false; |
|
527 } |
|
528 |
|
529 const uint32_t rate = mInfo.mAudio.mRate; |
|
530 uint64_t tstamp_usecs = tstamp / NS_PER_USEC; |
|
531 if (mAudioStartUsec == -1) { |
|
532 // This is the first audio chunk. Assume the start time of our decode |
|
533 // is the start of this chunk. |
|
534 mAudioStartUsec = tstamp_usecs; |
|
535 } |
|
536 // If there's a gap between the start of this audio chunk and the end of |
|
537 // the previous audio chunk, we need to increment the packet count so that |
|
538 // the vorbis decode doesn't use data from before the gap to help decode |
|
539 // from after the gap. |
|
540 CheckedInt64 tstamp_frames = UsecsToFrames(tstamp_usecs, rate); |
|
541 CheckedInt64 decoded_frames = UsecsToFrames(mAudioStartUsec, rate); |
|
542 if (!tstamp_frames.isValid() || !decoded_frames.isValid()) { |
|
543 NS_WARNING("Int overflow converting WebM times to frames"); |
|
544 return false; |
|
545 } |
|
546 decoded_frames += mAudioFrames; |
|
547 if (!decoded_frames.isValid()) { |
|
548 NS_WARNING("Int overflow adding decoded_frames"); |
|
549 return false; |
|
550 } |
|
551 if (tstamp_frames.value() > decoded_frames.value()) { |
|
552 #ifdef DEBUG |
|
553 CheckedInt64 usecs = FramesToUsecs(tstamp_frames.value() - decoded_frames.value(), rate); |
|
554 LOG(PR_LOG_DEBUG, ("WebMReader detected gap of %lld, %lld frames, in audio stream\n", |
|
555 usecs.isValid() ? usecs.value() : -1, |
|
556 tstamp_frames.value() - decoded_frames.value())); |
|
557 #endif |
|
558 mPacketCount++; |
|
559 mAudioStartUsec = tstamp_usecs; |
|
560 mAudioFrames = 0; |
|
561 } |
|
562 |
|
563 int32_t total_frames = 0; |
|
564 for (uint32_t i = 0; i < count; ++i) { |
|
565 unsigned char* data; |
|
566 size_t length; |
|
567 r = nestegg_packet_data(aPacket, i, &data, &length); |
|
568 if (r == -1) { |
|
569 return false; |
|
570 } |
|
571 if (mAudioCodec == NESTEGG_CODEC_VORBIS) { |
|
572 ogg_packet opacket = InitOggPacket(data, length, false, false, -1); |
|
573 |
|
574 if (vorbis_synthesis(&mVorbisBlock, &opacket) != 0) { |
|
575 return false; |
|
576 } |
|
577 |
|
578 if (vorbis_synthesis_blockin(&mVorbisDsp, |
|
579 &mVorbisBlock) != 0) { |
|
580 return false; |
|
581 } |
|
582 |
|
583 VorbisPCMValue** pcm = 0; |
|
584 int32_t frames = 0; |
|
585 while ((frames = vorbis_synthesis_pcmout(&mVorbisDsp, &pcm)) > 0) { |
|
586 nsAutoArrayPtr<AudioDataValue> buffer(new AudioDataValue[frames * mChannels]); |
|
587 for (uint32_t j = 0; j < mChannels; ++j) { |
|
588 VorbisPCMValue* channel = pcm[j]; |
|
589 for (uint32_t i = 0; i < uint32_t(frames); ++i) { |
|
590 buffer[i*mChannels + j] = MOZ_CONVERT_VORBIS_SAMPLE(channel[i]); |
|
591 } |
|
592 } |
|
593 |
|
594 CheckedInt64 duration = FramesToUsecs(frames, rate); |
|
595 if (!duration.isValid()) { |
|
596 NS_WARNING("Int overflow converting WebM audio duration"); |
|
597 return false; |
|
598 } |
|
599 CheckedInt64 total_duration = FramesToUsecs(total_frames, rate); |
|
600 if (!total_duration.isValid()) { |
|
601 NS_WARNING("Int overflow converting WebM audio total_duration"); |
|
602 return false; |
|
603 } |
|
604 |
|
605 CheckedInt64 time = total_duration + tstamp_usecs; |
|
606 if (!time.isValid()) { |
|
607 NS_WARNING("Int overflow adding total_duration and tstamp_usecs"); |
|
608 return false; |
|
609 }; |
|
610 |
|
611 total_frames += frames; |
|
612 AudioQueue().Push(new AudioData(aOffset, |
|
613 time.value(), |
|
614 duration.value(), |
|
615 frames, |
|
616 buffer.forget(), |
|
617 mChannels)); |
|
618 mAudioFrames += frames; |
|
619 if (vorbis_synthesis_read(&mVorbisDsp, frames) != 0) { |
|
620 return false; |
|
621 } |
|
622 } |
|
623 } else if (mAudioCodec == NESTEGG_CODEC_OPUS) { |
|
624 #ifdef MOZ_OPUS |
|
625 uint32_t channels = mOpusParser->mChannels; |
|
626 |
|
627 // Maximum value is 63*2880, so there's no chance of overflow. |
|
628 int32_t frames_number = opus_packet_get_nb_frames(data, length); |
|
629 |
|
630 if (frames_number <= 0) |
|
631 return false; // Invalid packet header. |
|
632 int32_t samples = opus_packet_get_samples_per_frame(data, |
|
633 (opus_int32) rate); |
|
634 int32_t frames = frames_number*samples; |
|
635 |
|
636 // A valid Opus packet must be between 2.5 and 120 ms long. |
|
637 if (frames < 120 || frames > 5760) |
|
638 return false; |
|
639 nsAutoArrayPtr<AudioDataValue> buffer(new AudioDataValue[frames * channels]); |
|
640 |
|
641 // Decode to the appropriate sample type. |
|
642 #ifdef MOZ_SAMPLE_TYPE_FLOAT32 |
|
643 int ret = opus_multistream_decode_float(mOpusDecoder, |
|
644 data, length, |
|
645 buffer, frames, false); |
|
646 #else |
|
647 int ret = opus_multistream_decode(mOpusDecoder, |
|
648 data, length, |
|
649 buffer, frames, false); |
|
650 #endif |
|
651 if (ret < 0) |
|
652 return false; |
|
653 NS_ASSERTION(ret == frames, "Opus decoded too few audio samples"); |
|
654 CheckedInt64 startTime = tstamp_usecs; |
|
655 |
|
656 // Trim the initial frames while the decoder is settling. |
|
657 if (mSkip > 0) { |
|
658 int32_t skipFrames = std::min(mSkip, frames); |
|
659 if (skipFrames == frames) { |
|
660 // discard the whole packet |
|
661 mSkip -= frames; |
|
662 LOG(PR_LOG_DEBUG, ("Opus decoder skipping %d frames" |
|
663 " (whole packet)", frames)); |
|
664 return true; |
|
665 } |
|
666 int32_t keepFrames = frames - skipFrames; |
|
667 if (keepFrames < 0) { |
|
668 NS_WARNING("Int overflow in keepFrames"); |
|
669 return false; |
|
670 } |
|
671 int samples = keepFrames * channels; |
|
672 if (samples < 0) { |
|
673 NS_WARNING("Int overflow in samples"); |
|
674 return false; |
|
675 } |
|
676 nsAutoArrayPtr<AudioDataValue> trimBuffer(new AudioDataValue[samples]); |
|
677 for (int i = 0; i < samples; i++) |
|
678 trimBuffer[i] = buffer[skipFrames*channels + i]; |
|
679 startTime = startTime + FramesToUsecs(skipFrames, rate); |
|
680 frames = keepFrames; |
|
681 buffer = trimBuffer; |
|
682 |
|
683 mSkip -= skipFrames; |
|
684 LOG(PR_LOG_DEBUG, ("Opus decoder skipping %d frames", skipFrames)); |
|
685 } |
|
686 |
|
687 int64_t discardPadding = 0; |
|
688 r = nestegg_packet_discard_padding(aPacket, &discardPadding); |
|
689 if (discardPadding > 0) { |
|
690 CheckedInt64 discardFrames = UsecsToFrames(discardPadding * NS_PER_USEC, rate); |
|
691 if (!discardFrames.isValid()) { |
|
692 NS_WARNING("Int overflow in DiscardPadding"); |
|
693 return false; |
|
694 } |
|
695 int32_t keepFrames = frames - discardFrames.value(); |
|
696 if (keepFrames > 0) { |
|
697 int samples = keepFrames * channels; |
|
698 if (samples < 0) { |
|
699 NS_WARNING("Int overflow in samples"); |
|
700 return false; |
|
701 } |
|
702 nsAutoArrayPtr<AudioDataValue> trimBuffer(new AudioDataValue[samples]); |
|
703 for (int i = 0; i < samples; i++) |
|
704 trimBuffer[i] = buffer[i]; |
|
705 frames = keepFrames; |
|
706 buffer = trimBuffer; |
|
707 } else { |
|
708 LOG(PR_LOG_DEBUG, ("Opus decoder discarding whole packet" |
|
709 " ( %d frames) as padding", frames)); |
|
710 return true; |
|
711 } |
|
712 } |
|
713 |
|
714 // Apply the header gain if one was specified. |
|
715 #ifdef MOZ_SAMPLE_TYPE_FLOAT32 |
|
716 if (mOpusParser->mGain != 1.0f) { |
|
717 float gain = mOpusParser->mGain; |
|
718 int samples = frames * channels; |
|
719 for (int i = 0; i < samples; i++) { |
|
720 buffer[i] *= gain; |
|
721 } |
|
722 } |
|
723 #else |
|
724 if (mOpusParser->mGain_Q16 != 65536) { |
|
725 int64_t gain_Q16 = mOpusParser->mGain_Q16; |
|
726 int samples = frames * channels; |
|
727 for (int i = 0; i < samples; i++) { |
|
728 int32_t val = static_cast<int32_t>((gain_Q16*buffer[i] + 32768)>>16); |
|
729 buffer[i] = static_cast<AudioDataValue>(MOZ_CLIP_TO_15(val)); |
|
730 } |
|
731 } |
|
732 #endif |
|
733 |
|
734 // No channel mapping for more than 8 channels. |
|
735 if (channels > 8) { |
|
736 return false; |
|
737 } |
|
738 |
|
739 CheckedInt64 duration = FramesToUsecs(frames, rate); |
|
740 if (!duration.isValid()) { |
|
741 NS_WARNING("Int overflow converting WebM audio duration"); |
|
742 return false; |
|
743 } |
|
744 CheckedInt64 time = startTime - mCodecDelay; |
|
745 if (!time.isValid()) { |
|
746 NS_WARNING("Int overflow shifting tstamp by codec delay"); |
|
747 return false; |
|
748 }; |
|
749 AudioQueue().Push(new AudioData(mDecoder->GetResource()->Tell(), |
|
750 time.value(), |
|
751 duration.value(), |
|
752 frames, |
|
753 buffer.forget(), |
|
754 mChannels)); |
|
755 |
|
756 mAudioFrames += frames; |
|
757 #else |
|
758 return false; |
|
759 #endif /* MOZ_OPUS */ |
|
760 } |
|
761 } |
|
762 |
|
763 return true; |
|
764 } |
|
765 |
|
766 nsReturnRef<NesteggPacketHolder> WebMReader::NextPacket(TrackType aTrackType) |
|
767 { |
|
768 // The packet queue that packets will be pushed on if they |
|
769 // are not the type we are interested in. |
|
770 WebMPacketQueue& otherPackets = |
|
771 aTrackType == VIDEO ? mAudioPackets : mVideoPackets; |
|
772 |
|
773 // The packet queue for the type that we are interested in. |
|
774 WebMPacketQueue &packets = |
|
775 aTrackType == VIDEO ? mVideoPackets : mAudioPackets; |
|
776 |
|
777 // Flag to indicate that we do need to playback these types of |
|
778 // packets. |
|
779 bool hasType = aTrackType == VIDEO ? mHasVideo : mHasAudio; |
|
780 |
|
781 // Flag to indicate that we do need to playback the other type |
|
782 // of track. |
|
783 bool hasOtherType = aTrackType == VIDEO ? mHasAudio : mHasVideo; |
|
784 |
|
785 // Track we are interested in |
|
786 uint32_t ourTrack = aTrackType == VIDEO ? mVideoTrack : mAudioTrack; |
|
787 |
|
788 // Value of other track |
|
789 uint32_t otherTrack = aTrackType == VIDEO ? mAudioTrack : mVideoTrack; |
|
790 |
|
791 nsAutoRef<NesteggPacketHolder> holder; |
|
792 |
|
793 if (packets.GetSize() > 0) { |
|
794 holder.own(packets.PopFront()); |
|
795 } else { |
|
796 // Keep reading packets until we find a packet |
|
797 // for the track we want. |
|
798 do { |
|
799 nestegg_packet* packet; |
|
800 int r = nestegg_read_packet(mContext, &packet); |
|
801 if (r <= 0) { |
|
802 return nsReturnRef<NesteggPacketHolder>(); |
|
803 } |
|
804 int64_t offset = mDecoder->GetResource()->Tell(); |
|
805 holder.own(new NesteggPacketHolder(packet, offset)); |
|
806 |
|
807 unsigned int track = 0; |
|
808 r = nestegg_packet_track(packet, &track); |
|
809 if (r == -1) { |
|
810 return nsReturnRef<NesteggPacketHolder>(); |
|
811 } |
|
812 |
|
813 if (hasOtherType && otherTrack == track) { |
|
814 // Save the packet for when we want these packets |
|
815 otherPackets.Push(holder.disown()); |
|
816 continue; |
|
817 } |
|
818 |
|
819 // The packet is for the track we want to play |
|
820 if (hasType && ourTrack == track) { |
|
821 break; |
|
822 } |
|
823 } while (true); |
|
824 } |
|
825 |
|
826 return holder.out(); |
|
827 } |
|
828 |
|
829 bool WebMReader::DecodeAudioData() |
|
830 { |
|
831 NS_ASSERTION(mDecoder->OnDecodeThread(), "Should be on decode thread."); |
|
832 |
|
833 nsAutoRef<NesteggPacketHolder> holder(NextPacket(AUDIO)); |
|
834 if (!holder) { |
|
835 return false; |
|
836 } |
|
837 |
|
838 return DecodeAudioPacket(holder->mPacket, holder->mOffset); |
|
839 } |
|
840 |
|
841 bool WebMReader::DecodeVideoFrame(bool &aKeyframeSkip, |
|
842 int64_t aTimeThreshold) |
|
843 { |
|
844 NS_ASSERTION(mDecoder->OnDecodeThread(), "Should be on decode thread."); |
|
845 |
|
846 // Record number of frames decoded and parsed. Automatically update the |
|
847 // stats counters using the AutoNotifyDecoded stack-based class. |
|
848 uint32_t parsed = 0, decoded = 0; |
|
849 AbstractMediaDecoder::AutoNotifyDecoded autoNotify(mDecoder, parsed, decoded); |
|
850 |
|
851 nsAutoRef<NesteggPacketHolder> holder(NextPacket(VIDEO)); |
|
852 if (!holder) { |
|
853 return false; |
|
854 } |
|
855 |
|
856 nestegg_packet* packet = holder->mPacket; |
|
857 unsigned int track = 0; |
|
858 int r = nestegg_packet_track(packet, &track); |
|
859 if (r == -1) { |
|
860 return false; |
|
861 } |
|
862 |
|
863 unsigned int count = 0; |
|
864 r = nestegg_packet_count(packet, &count); |
|
865 if (r == -1) { |
|
866 return false; |
|
867 } |
|
868 |
|
869 uint64_t tstamp = 0; |
|
870 r = nestegg_packet_tstamp(packet, &tstamp); |
|
871 if (r == -1) { |
|
872 return false; |
|
873 } |
|
874 |
|
875 // The end time of this frame is the start time of the next frame. Fetch |
|
876 // the timestamp of the next packet for this track. If we've reached the |
|
877 // end of the resource, use the file's duration as the end time of this |
|
878 // video frame. |
|
879 uint64_t next_tstamp = 0; |
|
880 nsAutoRef<NesteggPacketHolder> next_holder(NextPacket(VIDEO)); |
|
881 if (next_holder) { |
|
882 r = nestegg_packet_tstamp(next_holder->mPacket, &next_tstamp); |
|
883 if (r == -1) { |
|
884 return false; |
|
885 } |
|
886 PushVideoPacket(next_holder.disown()); |
|
887 } else { |
|
888 ReentrantMonitorAutoEnter decoderMon(mDecoder->GetReentrantMonitor()); |
|
889 int64_t endTime = mDecoder->GetEndMediaTime(); |
|
890 if (endTime == -1) { |
|
891 return false; |
|
892 } |
|
893 next_tstamp = endTime * NS_PER_USEC; |
|
894 } |
|
895 |
|
896 int64_t tstamp_usecs = tstamp / NS_PER_USEC; |
|
897 for (uint32_t i = 0; i < count; ++i) { |
|
898 unsigned char* data; |
|
899 size_t length; |
|
900 r = nestegg_packet_data(packet, i, &data, &length); |
|
901 if (r == -1) { |
|
902 return false; |
|
903 } |
|
904 |
|
905 vpx_codec_stream_info_t si; |
|
906 memset(&si, 0, sizeof(si)); |
|
907 si.sz = sizeof(si); |
|
908 if (mVideoCodec == NESTEGG_CODEC_VP8) { |
|
909 vpx_codec_peek_stream_info(vpx_codec_vp8_dx(), data, length, &si); |
|
910 } else if (mVideoCodec == NESTEGG_CODEC_VP9) { |
|
911 vpx_codec_peek_stream_info(vpx_codec_vp9_dx(), data, length, &si); |
|
912 } |
|
913 if (aKeyframeSkip && (!si.is_kf || tstamp_usecs < aTimeThreshold)) { |
|
914 // Skipping to next keyframe... |
|
915 parsed++; // Assume 1 frame per chunk. |
|
916 continue; |
|
917 } |
|
918 |
|
919 if (aKeyframeSkip && si.is_kf) { |
|
920 aKeyframeSkip = false; |
|
921 } |
|
922 |
|
923 if (vpx_codec_decode(&mVPX, data, length, nullptr, 0)) { |
|
924 return false; |
|
925 } |
|
926 |
|
927 // If the timestamp of the video frame is less than |
|
928 // the time threshold required then it is not added |
|
929 // to the video queue and won't be displayed. |
|
930 if (tstamp_usecs < aTimeThreshold) { |
|
931 parsed++; // Assume 1 frame per chunk. |
|
932 continue; |
|
933 } |
|
934 |
|
935 vpx_codec_iter_t iter = nullptr; |
|
936 vpx_image_t *img; |
|
937 |
|
938 while ((img = vpx_codec_get_frame(&mVPX, &iter))) { |
|
939 NS_ASSERTION(img->fmt == IMG_FMT_I420, "WebM image format is not I420"); |
|
940 |
|
941 // Chroma shifts are rounded down as per the decoding examples in the VP8 SDK |
|
942 VideoData::YCbCrBuffer b; |
|
943 b.mPlanes[0].mData = img->planes[0]; |
|
944 b.mPlanes[0].mStride = img->stride[0]; |
|
945 b.mPlanes[0].mHeight = img->d_h; |
|
946 b.mPlanes[0].mWidth = img->d_w; |
|
947 b.mPlanes[0].mOffset = b.mPlanes[0].mSkip = 0; |
|
948 |
|
949 b.mPlanes[1].mData = img->planes[1]; |
|
950 b.mPlanes[1].mStride = img->stride[1]; |
|
951 b.mPlanes[1].mHeight = (img->d_h + 1) >> img->y_chroma_shift; |
|
952 b.mPlanes[1].mWidth = (img->d_w + 1) >> img->x_chroma_shift; |
|
953 b.mPlanes[1].mOffset = b.mPlanes[1].mSkip = 0; |
|
954 |
|
955 b.mPlanes[2].mData = img->planes[2]; |
|
956 b.mPlanes[2].mStride = img->stride[2]; |
|
957 b.mPlanes[2].mHeight = (img->d_h + 1) >> img->y_chroma_shift; |
|
958 b.mPlanes[2].mWidth = (img->d_w + 1) >> img->x_chroma_shift; |
|
959 b.mPlanes[2].mOffset = b.mPlanes[2].mSkip = 0; |
|
960 |
|
961 IntRect picture = ToIntRect(mPicture); |
|
962 if (img->d_w != static_cast<uint32_t>(mInitialFrame.width) || |
|
963 img->d_h != static_cast<uint32_t>(mInitialFrame.height)) { |
|
964 // Frame size is different from what the container reports. This is legal |
|
965 // in WebM, and we will preserve the ratio of the crop rectangle as it |
|
966 // was reported relative to the picture size reported by the container. |
|
967 picture.x = (mPicture.x * img->d_w) / mInitialFrame.width; |
|
968 picture.y = (mPicture.y * img->d_h) / mInitialFrame.height; |
|
969 picture.width = (img->d_w * mPicture.width) / mInitialFrame.width; |
|
970 picture.height = (img->d_h * mPicture.height) / mInitialFrame.height; |
|
971 } |
|
972 |
|
973 VideoData *v = VideoData::Create(mInfo.mVideo, |
|
974 mDecoder->GetImageContainer(), |
|
975 holder->mOffset, |
|
976 tstamp_usecs, |
|
977 (next_tstamp / NS_PER_USEC) - tstamp_usecs, |
|
978 b, |
|
979 si.is_kf, |
|
980 -1, |
|
981 picture); |
|
982 if (!v) { |
|
983 return false; |
|
984 } |
|
985 parsed++; |
|
986 decoded++; |
|
987 NS_ASSERTION(decoded <= parsed, |
|
988 "Expect only 1 frame per chunk per packet in WebM..."); |
|
989 VideoQueue().Push(v); |
|
990 } |
|
991 } |
|
992 |
|
993 return true; |
|
994 } |
|
995 |
|
996 void |
|
997 WebMReader::PushVideoPacket(NesteggPacketHolder* aItem) |
|
998 { |
|
999 mVideoPackets.PushFront(aItem); |
|
1000 } |
|
1001 |
|
1002 nsresult WebMReader::Seek(int64_t aTarget, int64_t aStartTime, int64_t aEndTime, |
|
1003 int64_t aCurrentTime) |
|
1004 { |
|
1005 NS_ASSERTION(mDecoder->OnDecodeThread(), "Should be on decode thread."); |
|
1006 |
|
1007 LOG(PR_LOG_DEBUG, ("Reader [%p] for Decoder [%p]: About to seek to %fs", |
|
1008 this, mDecoder, aTarget/1000000.0)); |
|
1009 if (NS_FAILED(ResetDecode())) { |
|
1010 return NS_ERROR_FAILURE; |
|
1011 } |
|
1012 uint32_t trackToSeek = mHasVideo ? mVideoTrack : mAudioTrack; |
|
1013 uint64_t target = aTarget * NS_PER_USEC; |
|
1014 if (mSeekPreroll) { |
|
1015 target = std::max(static_cast<uint64_t>(aStartTime * NS_PER_USEC), target - mSeekPreroll); |
|
1016 } |
|
1017 int r = nestegg_track_seek(mContext, trackToSeek, target); |
|
1018 if (r != 0) { |
|
1019 // Try seeking directly based on cluster information in memory. |
|
1020 int64_t offset = 0; |
|
1021 bool rv = mBufferedState->GetOffsetForTime((aTarget - aStartTime)/NS_PER_USEC, &offset); |
|
1022 if (!rv) { |
|
1023 return NS_ERROR_FAILURE; |
|
1024 } |
|
1025 |
|
1026 r = nestegg_offset_seek(mContext, offset); |
|
1027 if (r != 0) { |
|
1028 return NS_ERROR_FAILURE; |
|
1029 } |
|
1030 } |
|
1031 return NS_OK; |
|
1032 } |
|
1033 |
|
1034 nsresult WebMReader::GetBuffered(dom::TimeRanges* aBuffered, int64_t aStartTime) |
|
1035 { |
|
1036 MediaResource* resource = mDecoder->GetResource(); |
|
1037 |
|
1038 uint64_t timecodeScale; |
|
1039 if (!mContext || nestegg_tstamp_scale(mContext, &timecodeScale) == -1) { |
|
1040 return NS_OK; |
|
1041 } |
|
1042 |
|
1043 // Special case completely cached files. This also handles local files. |
|
1044 bool isFullyCached = resource->IsDataCachedToEndOfResource(0); |
|
1045 if (isFullyCached) { |
|
1046 uint64_t duration = 0; |
|
1047 if (nestegg_duration(mContext, &duration) == 0) { |
|
1048 aBuffered->Add(0, duration / NS_PER_S); |
|
1049 } |
|
1050 } |
|
1051 |
|
1052 uint32_t bufferedLength = 0; |
|
1053 aBuffered->GetLength(&bufferedLength); |
|
1054 |
|
1055 // Either we the file is not fully cached, or we couldn't find a duration in |
|
1056 // the WebM bitstream. |
|
1057 if (!isFullyCached || !bufferedLength) { |
|
1058 MediaResource* resource = mDecoder->GetResource(); |
|
1059 nsTArray<MediaByteRange> ranges; |
|
1060 nsresult res = resource->GetCachedRanges(ranges); |
|
1061 NS_ENSURE_SUCCESS(res, res); |
|
1062 |
|
1063 for (uint32_t index = 0; index < ranges.Length(); index++) { |
|
1064 uint64_t start, end; |
|
1065 bool rv = mBufferedState->CalculateBufferedForRange(ranges[index].mStart, |
|
1066 ranges[index].mEnd, |
|
1067 &start, &end); |
|
1068 if (rv) { |
|
1069 double startTime = start * timecodeScale / NS_PER_S - aStartTime; |
|
1070 double endTime = end * timecodeScale / NS_PER_S - aStartTime; |
|
1071 // If this range extends to the end of the file, the true end time |
|
1072 // is the file's duration. |
|
1073 if (resource->IsDataCachedToEndOfResource(ranges[index].mStart)) { |
|
1074 uint64_t duration = 0; |
|
1075 if (nestegg_duration(mContext, &duration) == 0) { |
|
1076 endTime = duration / NS_PER_S; |
|
1077 } |
|
1078 } |
|
1079 |
|
1080 aBuffered->Add(startTime, endTime); |
|
1081 } |
|
1082 } |
|
1083 } |
|
1084 |
|
1085 return NS_OK; |
|
1086 } |
|
1087 |
|
1088 void WebMReader::NotifyDataArrived(const char* aBuffer, uint32_t aLength, int64_t aOffset) |
|
1089 { |
|
1090 mBufferedState->NotifyDataArrived(aBuffer, aLength, aOffset); |
|
1091 } |
|
1092 |
|
1093 } // namespace mozilla |