| OLD | NEW |
| 1 // Copyright 2014 The Chromium Authors. All rights reserved. | 1 // Copyright 2014 The Chromium Authors. All rights reserved. |
| 2 // Use of this source code is governed by a BSD-style license that can be | 2 // Use of this source code is governed by a BSD-style license that can be |
| 3 // found in the LICENSE file. | 3 // found in the LICENSE file. |
| 4 | 4 |
| 5 #include "chromecast/media/cma/pipeline/av_pipeline_impl.h" | 5 #include "chromecast/media/cma/pipeline/av_pipeline_impl.h" |
| 6 | 6 |
| 7 #include "base/bind.h" | 7 #include "base/bind.h" |
| 8 #include "base/location.h" | 8 #include "base/location.h" |
| 9 #include "base/message_loop/message_loop_proxy.h" | 9 #include "base/message_loop/message_loop_proxy.h" |
| 10 #include "base/strings/string_number_conversions.h" | 10 #include "base/strings/string_number_conversions.h" |
| (...skipping 27 matching lines...) Expand all Loading... |
| 38 media_component_device_(media_component_device), | 38 media_component_device_(media_component_device), |
| 39 state_(kUninitialized), | 39 state_(kUninitialized), |
| 40 buffered_time_(::media::kNoTimestamp()), | 40 buffered_time_(::media::kNoTimestamp()), |
| 41 playable_buffered_time_(::media::kNoTimestamp()), | 41 playable_buffered_time_(::media::kNoTimestamp()), |
| 42 enable_feeding_(false), | 42 enable_feeding_(false), |
| 43 pending_read_(false), | 43 pending_read_(false), |
| 44 pending_push_(false), | 44 pending_push_(false), |
| 45 enable_time_update_(false), | 45 enable_time_update_(false), |
| 46 pending_time_update_task_(false), | 46 pending_time_update_task_(false), |
| 47 media_keys_(NULL), | 47 media_keys_(NULL), |
| 48 media_keys_callback_id_(kNoCallbackId), | 48 media_keys_callback_id_(kNoCallbackId) { |
| 49 weak_factory_(this) { | |
| 50 DCHECK(media_component_device); | 49 DCHECK(media_component_device); |
| 51 weak_this_ = weak_factory_.GetWeakPtr(); | |
| 52 thread_checker_.DetachFromThread(); | 50 thread_checker_.DetachFromThread(); |
| 53 } | 51 } |
| 54 | 52 |
| 55 AvPipelineImpl::~AvPipelineImpl() { | 53 AvPipelineImpl::~AvPipelineImpl() { |
| 56 // If there are weak pointers in the wild, they must be invalidated | 54 // If there are weak pointers in the wild, they must be invalidated |
| 57 // on the right thread. | 55 // on the right thread. |
| 58 DCHECK(thread_checker_.CalledOnValidThread()); | 56 DCHECK(thread_checker_.CalledOnValidThread()); |
| 59 media_component_device_->SetClient(MediaComponentDevice::Client()); | 57 media_component_device_->SetClient(MediaComponentDevice::Client()); |
| 60 | 58 |
| 61 if (media_keys_ && media_keys_callback_id_ != kNoCallbackId) | 59 { |
| 62 media_keys_->UnregisterPlayer(media_keys_callback_id_); | 60 base::AutoLock lock(media_keys_lock_); |
| 61 if (media_keys_ && media_keys_callback_id_ != kNoCallbackId) |
| 62 media_keys_->UnregisterPlayer(media_keys_callback_id_); |
| 63 } |
| 63 } | 64 } |
| 64 | 65 |
| 65 void AvPipelineImpl::TransitionToState(State state) { | 66 void AvPipelineImpl::TransitionToState(State state) { |
| 66 DCHECK(thread_checker_.CalledOnValidThread()); | 67 DCHECK(thread_checker_.CalledOnValidThread()); |
| 67 state_ = state; | 68 state_ = state; |
| 68 } | 69 } |
| 69 | 70 |
| 70 void AvPipelineImpl::SetCodedFrameProvider( | 71 void AvPipelineImpl::SetCodedFrameProvider( |
| 71 scoped_ptr<CodedFrameProvider> frame_provider, | 72 scoped_ptr<CodedFrameProvider> frame_provider, |
| 72 size_t max_buffer_size, | 73 size_t max_buffer_size, |
| 73 size_t max_frame_size) { | 74 size_t max_frame_size) { |
| 74 DCHECK_EQ(state_, kUninitialized); | 75 DCHECK_EQ(state_, kUninitialized); |
| 75 DCHECK(frame_provider); | 76 DCHECK(frame_provider); |
| 76 | 77 |
| 77 // Wrap the incoming frame provider to add some buffering capabilities. | 78 // Wrap the incoming frame provider to add some buffering capabilities. |
| 78 frame_provider_.reset( | 79 frame_provider_.reset( |
| 79 new BufferingFrameProvider( | 80 new BufferingFrameProvider( |
| 80 frame_provider.Pass(), | 81 frame_provider.Pass(), |
| 81 max_buffer_size, | 82 max_buffer_size, |
| 82 max_frame_size, | 83 max_frame_size, |
| 83 base::Bind(&AvPipelineImpl::OnFrameBuffered, weak_this_))); | 84 base::Bind(&AvPipelineImpl::OnFrameBuffered, this))); |
| 84 } | 85 } |
| 85 | 86 |
| 86 void AvPipelineImpl::SetClient(const AvPipelineClient& client) { | 87 void AvPipelineImpl::SetClient(const AvPipelineClient& client) { |
| 87 DCHECK(thread_checker_.CalledOnValidThread()); | 88 DCHECK(thread_checker_.CalledOnValidThread()); |
| 88 DCHECK_EQ(state_, kUninitialized); | 89 DCHECK_EQ(state_, kUninitialized); |
| 89 client_ = client; | 90 client_ = client; |
| 90 } | 91 } |
| 91 | 92 |
| 92 bool AvPipelineImpl::Initialize() { | 93 bool AvPipelineImpl::Initialize() { |
| 93 DCHECK(thread_checker_.CalledOnValidThread()); | 94 DCHECK(thread_checker_.CalledOnValidThread()); |
| 94 DCHECK_EQ(state_, kUninitialized); | 95 DCHECK_EQ(state_, kUninitialized); |
| 95 | 96 |
| 96 MediaComponentDevice::Client client; | 97 MediaComponentDevice::Client client; |
| 97 client.eos_cb = base::Bind(&AvPipelineImpl::OnEos, weak_this_); | 98 client.eos_cb = base::Bind(&AvPipelineImpl::OnEos, this); |
| 98 media_component_device_->SetClient(client); | 99 media_component_device_->SetClient(client); |
| 99 if (!media_component_device_->SetState(MediaComponentDevice::kStateIdle)) | 100 if (!media_component_device_->SetState(MediaComponentDevice::kStateIdle)) |
| 100 return false; | 101 return false; |
| 101 | 102 |
| 102 return true; | 103 return true; |
| 103 } | 104 } |
| 104 | 105 |
| 105 bool AvPipelineImpl::StartPlayingFrom( | 106 bool AvPipelineImpl::StartPlayingFrom( |
| 106 base::TimeDelta time, | 107 base::TimeDelta time, |
| 107 const scoped_refptr<BufferingState>& buffering_state) { | 108 const scoped_refptr<BufferingState>& buffering_state) { |
| (...skipping 13 matching lines...) Expand all Loading... |
| 121 if (buffering_state_.get()) | 122 if (buffering_state_.get()) |
| 122 buffering_state_->SetMediaTime(time); | 123 buffering_state_->SetMediaTime(time); |
| 123 | 124 |
| 124 if (!media_component_device_->SetState(MediaComponentDevice::kStateRunning)) | 125 if (!media_component_device_->SetState(MediaComponentDevice::kStateRunning)) |
| 125 return false; | 126 return false; |
| 126 | 127 |
| 127 // Start feeding the pipeline. | 128 // Start feeding the pipeline. |
| 128 enable_feeding_ = true; | 129 enable_feeding_ = true; |
| 129 base::MessageLoopProxy::current()->PostTask( | 130 base::MessageLoopProxy::current()->PostTask( |
| 130 FROM_HERE, | 131 FROM_HERE, |
| 131 base::Bind(&AvPipelineImpl::FetchBufferIfNeeded, weak_this_)); | 132 base::Bind(&AvPipelineImpl::FetchBufferIfNeeded, this)); |
| 132 | 133 |
| 133 return true; | 134 return true; |
| 134 } | 135 } |
| 135 | 136 |
| 136 void AvPipelineImpl::Flush(const base::Closure& done_cb) { | 137 void AvPipelineImpl::Flush(const base::Closure& done_cb) { |
| 137 DCHECK(thread_checker_.CalledOnValidThread()); | 138 DCHECK(thread_checker_.CalledOnValidThread()); |
| 138 DCHECK_EQ(state_, kFlushing); | 139 DCHECK_EQ(state_, kFlushing); |
| 139 DCHECK_EQ( | 140 DCHECK_EQ( |
| 140 media_component_device_->GetState(), MediaComponentDevice::kStateRunning); | 141 media_component_device_->GetState(), MediaComponentDevice::kStateRunning); |
| 141 // Note: returning to idle state aborts any pending frame push. | 142 // Note: returning to idle state aborts any pending frame push. |
| (...skipping 34 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 176 if (media_component_device_->GetState() == MediaComponentDevice::kStateIdle) { | 177 if (media_component_device_->GetState() == MediaComponentDevice::kStateIdle) { |
| 177 media_component_device_->SetState( | 178 media_component_device_->SetState( |
| 178 MediaComponentDevice::kStateUninitialized); | 179 MediaComponentDevice::kStateUninitialized); |
| 179 } | 180 } |
| 180 } | 181 } |
| 181 | 182 |
| 182 void AvPipelineImpl::SetCdm(BrowserCdmCast* media_keys) { | 183 void AvPipelineImpl::SetCdm(BrowserCdmCast* media_keys) { |
| 183 DCHECK(thread_checker_.CalledOnValidThread()); | 184 DCHECK(thread_checker_.CalledOnValidThread()); |
| 184 DCHECK(media_keys); | 185 DCHECK(media_keys); |
| 185 | 186 |
| 186 if (media_keys_ && media_keys_callback_id_ != kNoCallbackId) | 187 { |
| 187 media_keys_->UnregisterPlayer(media_keys_callback_id_); | 188 base::AutoLock lock(media_keys_lock_); |
| 189 if (media_keys_ && media_keys_callback_id_ != kNoCallbackId) |
| 190 media_keys_->UnregisterPlayer(media_keys_callback_id_); |
| 188 | 191 |
| 189 media_keys_ = media_keys; | 192 media_keys_ = media_keys; |
| 190 media_keys_callback_id_ = media_keys_->RegisterPlayer( | 193 media_keys_callback_id_ = media_keys_->RegisterPlayer( |
| 191 ::media::BindToCurrentLoop( | 194 ::media::BindToCurrentLoop( |
| 192 base::Bind(&AvPipelineImpl::OnCdmStateChanged, weak_this_)), | 195 base::Bind(&AvPipelineImpl::OnCdmStateChanged, this)), |
| 193 ::media::BindToCurrentLoop( | 196 // Note: this callback gets invoked in ~BrowserCdmCast. Posting |
| 194 base::Bind(&AvPipelineImpl::OnCdmDestroyed, weak_this_))); | 197 // OnCdmDestroyed to the media thread results in a race condition |
| 198 // with media_keys_ accesses. This callback must run synchronously, |
| 199 // otherwise media_keys_ access might occur after it is deleted. |
| 200 base::Bind(&AvPipelineImpl::OnCdmDestroyed, this)); |
| 201 } |
| 195 } | 202 } |
| 196 | 203 |
| 197 void AvPipelineImpl::OnEos() { | 204 void AvPipelineImpl::OnEos() { |
| 198 DCHECK(thread_checker_.CalledOnValidThread()); | 205 DCHECK(thread_checker_.CalledOnValidThread()); |
| 199 CMALOG(kLogControl) << __FUNCTION__; | 206 CMALOG(kLogControl) << __FUNCTION__; |
| 200 if (state_ != kPlaying) | 207 if (state_ != kPlaying) |
| 201 return; | 208 return; |
| 202 | 209 |
| 203 if (!client_.eos_cb.is_null()) | 210 if (!client_.eos_cb.is_null()) |
| 204 client_.eos_cb.Run(); | 211 client_.eos_cb.Run(); |
| 205 } | 212 } |
| 206 | 213 |
| 207 void AvPipelineImpl::FetchBufferIfNeeded() { | 214 void AvPipelineImpl::FetchBufferIfNeeded() { |
| 208 DCHECK(thread_checker_.CalledOnValidThread()); | 215 DCHECK(thread_checker_.CalledOnValidThread()); |
| 209 if (!enable_feeding_) | 216 if (!enable_feeding_) |
| 210 return; | 217 return; |
| 211 | 218 |
| 212 if (pending_read_ || pending_buffer_.get()) | 219 if (pending_read_ || pending_buffer_.get()) |
| 213 return; | 220 return; |
| 214 | 221 |
| 215 pending_read_ = true; | 222 pending_read_ = true; |
| 216 frame_provider_->Read( | 223 frame_provider_->Read( |
| 217 base::Bind(&AvPipelineImpl::OnNewFrame, weak_this_)); | 224 base::Bind(&AvPipelineImpl::OnNewFrame, this)); |
| 218 } | 225 } |
| 219 | 226 |
| 220 void AvPipelineImpl::OnNewFrame( | 227 void AvPipelineImpl::OnNewFrame( |
| 221 const scoped_refptr<DecoderBufferBase>& buffer, | 228 const scoped_refptr<DecoderBufferBase>& buffer, |
| 222 const ::media::AudioDecoderConfig& audio_config, | 229 const ::media::AudioDecoderConfig& audio_config, |
| 223 const ::media::VideoDecoderConfig& video_config) { | 230 const ::media::VideoDecoderConfig& video_config) { |
| 224 DCHECK(thread_checker_.CalledOnValidThread()); | 231 DCHECK(thread_checker_.CalledOnValidThread()); |
| 225 pending_read_ = false; | 232 pending_read_ = false; |
| 226 | 233 |
| 227 if (audio_config.IsValidConfig() || video_config.IsValidConfig()) | 234 if (audio_config.IsValidConfig() || video_config.IsValidConfig()) |
| 228 update_config_cb_.Run(audio_config, video_config); | 235 update_config_cb_.Run(audio_config, video_config); |
| 229 | 236 |
| 230 pending_buffer_ = buffer; | 237 pending_buffer_ = buffer; |
| 231 ProcessPendingBuffer(); | 238 ProcessPendingBuffer(); |
| 232 | 239 |
| 233 base::MessageLoopProxy::current()->PostTask( | 240 base::MessageLoopProxy::current()->PostTask( |
| 234 FROM_HERE, | 241 FROM_HERE, |
| 235 base::Bind(&AvPipelineImpl::FetchBufferIfNeeded, weak_this_)); | 242 base::Bind(&AvPipelineImpl::FetchBufferIfNeeded, this)); |
| 236 } | 243 } |
| 237 | 244 |
| 238 void AvPipelineImpl::ProcessPendingBuffer() { | 245 void AvPipelineImpl::ProcessPendingBuffer() { |
| 239 if (!enable_feeding_) | 246 if (!enable_feeding_) |
| 240 return; | 247 return; |
| 241 | 248 |
| 242 // Initiate a read if there isn't already one. | 249 // Initiate a read if there isn't already one. |
| 243 if (!pending_buffer_.get() && !pending_read_) { | 250 if (!pending_buffer_.get() && !pending_read_) { |
| 244 base::MessageLoopProxy::current()->PostTask( | 251 base::MessageLoopProxy::current()->PostTask( |
| 245 FROM_HERE, | 252 FROM_HERE, |
| 246 base::Bind(&AvPipelineImpl::FetchBufferIfNeeded, weak_this_)); | 253 base::Bind(&AvPipelineImpl::FetchBufferIfNeeded, this)); |
| 247 return; | 254 return; |
| 248 } | 255 } |
| 249 | 256 |
| 250 if (!pending_buffer_.get() || pending_push_) | 257 if (!pending_buffer_.get() || pending_push_) |
| 251 return; | 258 return; |
| 252 | 259 |
| 253 // Break the feeding loop when the end of stream is reached. | 260 // Break the feeding loop when the end of stream is reached. |
| 254 if (pending_buffer_->end_of_stream()) { | 261 if (pending_buffer_->end_of_stream()) { |
| 255 CMALOG(kLogControl) << __FUNCTION__ << ": EOS reached, stopped feeding"; | 262 CMALOG(kLogControl) << __FUNCTION__ << ": EOS reached, stopped feeding"; |
| 256 enable_feeding_ = false; | 263 enable_feeding_ = false; |
| 257 } | 264 } |
| 258 | 265 |
| 259 scoped_refptr<DecryptContext> decrypt_context; | 266 scoped_refptr<DecryptContext> decrypt_context; |
| 260 if (!pending_buffer_->end_of_stream() && | 267 if (!pending_buffer_->end_of_stream() && |
| 261 pending_buffer_->decrypt_config()) { | 268 pending_buffer_->decrypt_config()) { |
| 262 // Verify that CDM has the key ID. | 269 // Verify that CDM has the key ID. |
| 263 // Should not send the frame if the key ID is not available yet. | 270 // Should not send the frame if the key ID is not available yet. |
| 264 std::string key_id(pending_buffer_->decrypt_config()->key_id()); | 271 std::string key_id(pending_buffer_->decrypt_config()->key_id()); |
| 265 if (!media_keys_) { | 272 { |
| 266 CMALOG(kLogControl) << "No CDM for frame: pts=" | 273 base::AutoLock lock(media_keys_lock_); |
| 267 << pending_buffer_->timestamp().InMilliseconds(); | 274 if (!media_keys_) { |
| 268 return; | 275 CMALOG(kLogControl) << "No CDM for frame: pts=" |
| 276 << pending_buffer_->timestamp().InMilliseconds(); |
| 277 return; |
| 278 } |
| 279 decrypt_context = media_keys_->GetDecryptContext(key_id); |
| 269 } | 280 } |
| 270 decrypt_context = media_keys_->GetDecryptContext(key_id); | |
| 271 if (!decrypt_context.get()) { | 281 if (!decrypt_context.get()) { |
| 272 CMALOG(kLogControl) << "frame(pts=" | 282 CMALOG(kLogControl) << "frame(pts=" |
| 273 << pending_buffer_->timestamp().InMilliseconds() | 283 << pending_buffer_->timestamp().InMilliseconds() |
| 274 << "): waiting for key id " | 284 << "): waiting for key id " |
| 275 << base::HexEncode(&key_id[0], key_id.size()); | 285 << base::HexEncode(&key_id[0], key_id.size()); |
| 276 return; | 286 return; |
| 277 } | 287 } |
| 278 | 288 |
| 279 // If we do have the clear key, decrypt the pending buffer | 289 // If we do have the clear key, decrypt the pending buffer |
| 280 // and reset the decryption context (not needed anymore). | 290 // and reset the decryption context (not needed anymore). |
| 281 crypto::SymmetricKey* key = decrypt_context->GetKey(); | 291 crypto::SymmetricKey* key = decrypt_context->GetKey(); |
| 282 if (key != NULL) { | 292 if (key != NULL) { |
| 283 pending_buffer_ = DecryptDecoderBuffer(pending_buffer_, key); | 293 pending_buffer_ = DecryptDecoderBuffer(pending_buffer_, key); |
| 284 decrypt_context = scoped_refptr<DecryptContext>(); | 294 decrypt_context = scoped_refptr<DecryptContext>(); |
| 285 } | 295 } |
| 286 } | 296 } |
| 287 | 297 |
| 288 if (!pending_buffer_->end_of_stream() && buffering_state_.get()) { | 298 if (!pending_buffer_->end_of_stream() && buffering_state_.get()) { |
| 289 base::TimeDelta timestamp = pending_buffer_->timestamp(); | 299 base::TimeDelta timestamp = pending_buffer_->timestamp(); |
| 290 if (timestamp != ::media::kNoTimestamp()) | 300 if (timestamp != ::media::kNoTimestamp()) |
| 291 buffering_state_->SetMaxRenderingTime(timestamp); | 301 buffering_state_->SetMaxRenderingTime(timestamp); |
| 292 } | 302 } |
| 293 | 303 |
| 294 MediaComponentDevice::FrameStatus status = media_component_device_->PushFrame( | 304 MediaComponentDevice::FrameStatus status = media_component_device_->PushFrame( |
| 295 decrypt_context, | 305 decrypt_context, |
| 296 pending_buffer_, | 306 pending_buffer_, |
| 297 base::Bind(&AvPipelineImpl::OnFramePushed, weak_this_)); | 307 base::Bind(&AvPipelineImpl::OnFramePushed, this)); |
| 298 pending_buffer_ = scoped_refptr<DecoderBufferBase>(); | 308 pending_buffer_ = scoped_refptr<DecoderBufferBase>(); |
| 299 | 309 |
| 300 pending_push_ = (status == MediaComponentDevice::kFramePending); | 310 pending_push_ = (status == MediaComponentDevice::kFramePending); |
| 301 if (!pending_push_) | 311 if (!pending_push_) |
| 302 OnFramePushed(status); | 312 OnFramePushed(status); |
| 303 } | 313 } |
| 304 | 314 |
| 305 void AvPipelineImpl::OnFramePushed(MediaComponentDevice::FrameStatus status) { | 315 void AvPipelineImpl::OnFramePushed(MediaComponentDevice::FrameStatus status) { |
| 306 DCHECK(thread_checker_.CalledOnValidThread()); | 316 DCHECK(thread_checker_.CalledOnValidThread()); |
| 307 pending_push_ = false; | 317 pending_push_ = false; |
| 308 if (status == MediaComponentDevice::kFrameFailed) { | 318 if (status == MediaComponentDevice::kFrameFailed) { |
| 309 LOG(WARNING) << "AvPipelineImpl: PushFrame failed"; | 319 LOG(WARNING) << "AvPipelineImpl: PushFrame failed"; |
| 310 enable_feeding_ = false; | 320 enable_feeding_ = false; |
| 311 state_ = kError; | 321 state_ = kError; |
| 312 return; | 322 return; |
| 313 } | 323 } |
| 314 base::MessageLoopProxy::current()->PostTask( | 324 base::MessageLoopProxy::current()->PostTask( |
| 315 FROM_HERE, | 325 FROM_HERE, |
| 316 base::Bind(&AvPipelineImpl::ProcessPendingBuffer, weak_this_)); | 326 base::Bind(&AvPipelineImpl::ProcessPendingBuffer, this)); |
| 317 } | 327 } |
| 318 | 328 |
| 319 void AvPipelineImpl::OnCdmStateChanged() { | 329 void AvPipelineImpl::OnCdmStateChanged() { |
| 320 DCHECK(thread_checker_.CalledOnValidThread()); | 330 DCHECK(thread_checker_.CalledOnValidThread()); |
| 321 | 331 |
| 322 // Update the buffering state if needed. | 332 // Update the buffering state if needed. |
| 323 if (buffering_state_.get()) | 333 if (buffering_state_.get()) |
| 324 UpdatePlayableFrames(); | 334 UpdatePlayableFrames(); |
| 325 | 335 |
| 326 // Process the pending buffer in case the CDM now has the frame key id. | 336 // Process the pending buffer in case the CDM now has the frame key id. |
| 327 ProcessPendingBuffer(); | 337 ProcessPendingBuffer(); |
| 328 } | 338 } |
| 329 | 339 |
| 330 void AvPipelineImpl::OnCdmDestroyed() { | 340 void AvPipelineImpl::OnCdmDestroyed() { |
| 331 DCHECK(thread_checker_.CalledOnValidThread()); | 341 base::AutoLock lock(media_keys_lock_); |
| 332 media_keys_ = NULL; | 342 media_keys_ = NULL; |
| 333 } | 343 } |
| 334 | 344 |
| 335 void AvPipelineImpl::OnFrameBuffered( | 345 void AvPipelineImpl::OnFrameBuffered( |
| 336 const scoped_refptr<DecoderBufferBase>& buffer, | 346 const scoped_refptr<DecoderBufferBase>& buffer, |
| 337 bool is_at_max_capacity) { | 347 bool is_at_max_capacity) { |
| 338 DCHECK(thread_checker_.CalledOnValidThread()); | 348 DCHECK(thread_checker_.CalledOnValidThread()); |
| 339 | 349 |
| 340 if (!buffering_state_.get()) | 350 if (!buffering_state_.get()) |
| 341 return; | 351 return; |
| (...skipping 40 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 382 } | 392 } |
| 383 } | 393 } |
| 384 | 394 |
| 385 // The frame is playable: remove it from the list of non playable frames. | 395 // The frame is playable: remove it from the list of non playable frames. |
| 386 non_playable_frames_.pop_front(); | 396 non_playable_frames_.pop_front(); |
| 387 } | 397 } |
| 388 } | 398 } |
| 389 | 399 |
| 390 } // namespace media | 400 } // namespace media |
| 391 } // namespace chromecast | 401 } // namespace chromecast |
| OLD | NEW |