root/trunk/whispercast/stream_request.cc

Revision 7, 20.7 kB (checked in by whispercastorg, 2 years ago)

version 0.2.0

Line 
1 // Copyright (c) 2009, Whispersoft s.r.l.
2 // All rights reserved.
3 //
4 // Redistribution and use in source and binary forms, with or without
5 // modification, are permitted provided that the following conditions are
6 // met:
7 //
8 // * Redistributions of source code must retain the above copyright
9 // notice, this list of conditions and the following disclaimer.
10 // * Redistributions in binary form must reproduce the above
11 // copyright notice, this list of conditions and the following disclaimer
12 // in the documentation and/or other materials provided with the
13 // distribution.
14 // * Neither the name of Whispersoft s.r.l. nor the names of its
15 // contributors may be used to endorse or promote products derived from
16 // this software without specific prior written permission.
17 //
18 // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
19 // "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
20 // LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
21 // A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
22 // OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
23 // SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
24 // LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
25 // DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
26 // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
27 // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28 // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
29 //
30 // Author: Catalin Popescu
31
32 #include <vector>
33 #include "stream_request.h"
34 #include <whisperlib/net/util/ipclassifier.h>
35
36 #include <whisperstreamlib/internal/internal_frame.h>
37
38 #include <whisperstreamlib/aac/aac_tag_splitter.h>
39 #include <whisperstreamlib/flv/flv_tag_splitter.h>
40 #include <whisperstreamlib/mp3/mp3_tag_splitter.h>
41 #include <whisperstreamlib/internal/internal_tag_splitter.h>
42 #include <whisperstreamlib/raw/raw_tag_splitter.h>
43
44 //////////////////////////////////////////////////////////////////////
45
46 DECLARE_int32(http_port);
47 DECLARE_int64(http_connection_write_ahead_ms);
48 DECLARE_int32(http_connection_max_media_outbuf_size);
49
50 DEFINE_bool(stream_log_detailed_media_tags,
51             false,
52             "Log each media tag that we send -- really detailed :)");
53
54 //////////////////////////////////////////////////////////////////////
55
56 static
57 const char* ResultName(http::HttpReturnCode result) {
58   if ( result >= 200 && result < 300 )
59     return "SUCCESS";
60   if ( result >= 300 && result < 400 )
61     return "REDIRECTED";
62   if ( result >= 500 )
63     return "SERVER ERROR";
64
65   // We are left with 4xx and strange stuff
66   switch ( result ) {
67   case http::BAD_REQUEST:
68     return "INVALID REQUEST";
69   case http::UNAUTHORIZED:
70   case http::PAYMENT_REQUIRED:
71   case http::FORBIDDEN:
72   case http::METHOD_NOT_ALLOWED:
73   case http::NOT_ACCEPTABLE:
74   case http::PROXY_AUTHENTICATION_REQUIRED :
75     return "UNAUTHORIZED";
76   case http::NOT_FOUND:
77     return "NOT FOUND";
78   case http::REQUEST_TIME_OUT:
79     return "TIMEOUT";
80   default:
81     return "INVALID REQUEST";
82   }
83   return "UNKNOWN";
84 }
85
86 //////////////////////////////////////////////////////////////////////
87
88 StreamRequest::StreamRequest(
89     net::Selector* selector,
90     int64 connection_id,
91     streaming::StatsCollector* collector,
92     streaming::ElementMapper* mapper,
93     streaming::Request* media_request,
94     http::ServerRequest* http_request,
95     streaming::AuthorizeHelper* auth_helper)
96     : selector_(selector),
97       mapper_(mapper),
98       stats_collector_(collector),
99       media_request_(media_request),
100       http_request_(http_request),
101       auth_helper_(auth_helper),
102       element_seq_id_(0),
103       processing_callback_(
104           NewPermanentCallback(this, &StreamRequest::ProcessTag)),
105       tag_type_(streaming::INVALID_STREAM_TYPE),
106       serializer_(NULL),
107       unpause_buffer_callback_(NULL),
108       is_first_tag_(true),
109       is_audio_dropping_(false),
110       is_video_dropping_(false),
111       first_tag_time_(0),
112       last_tag_stream_time_(0),
113       media_id_(0),
114       start_time_(selector->now()),
115       reauthorize_callback_(NULL),
116       terminate_callback_(NULL) {
117   // Prepare connection stats
118   const int64 now = timer::Date::Now();
119   connection_begin_stats_.connection_id_ =
120       strutil::StringPrintf("%lld", static_cast<long long int>(connection_id));
121   connection_begin_stats_.timestamp_utc_ms_ = now;
122   connection_begin_stats_.remote_host_ =
123       http_request_->remote_address().ip_object().ToString();
124   connection_begin_stats_.remote_port_ =
125       http_request_->remote_address().port();
126   connection_begin_stats_.local_host_ = "127.0.0.1";
127   connection_begin_stats_.local_port_ = FLAGS_http_port;
128   connection_begin_stats_.protocol_ = "http";
129
130   // Prepare connection end stats for incrementing things..
131   connection_end_stats_.connection_id_ = connection_begin_stats_.connection_id_;
132   connection_end_stats_.timestamp_utc_ms_ = now;
133   connection_end_stats_.bytes_up_ = 0;
134   connection_end_stats_.bytes_down_ = 0;
135   connection_end_stats_.result_ = ConnectionResult("ACTIVE");
136
137   // Prepare stream stats (one stream per connection .. so far...)
138   stream_begin_stats_.stream_id_ = strutil::StringPrintf("%s.%lld",
139       connection_begin_stats_.connection_id_.Get().CStr(), 0LL);
140   stream_begin_stats_.timestamp_utc_ms_ = now;
141   stream_begin_stats_.connection_ = connection_begin_stats_;
142   stream_begin_stats_.session_id_ = media_request_->info().session_id_;
143   stream_begin_stats_.affiliate_id_ = media_request_->info().affiliate_id_;
144   stream_begin_stats_.client_id_ = media_request_->info().client_id_;
145   stream_begin_stats_.user_agent_ =
146     http_request_->request()->client_header()->FindField(
147           http::kHeaderUserAgent);
148
149   // Prepare stream end stats
150   stream_end_stats_.stream_id_ = stream_begin_stats_.stream_id_;
151   stream_end_stats_.timestamp_utc_ms_ = now;
152   stream_end_stats_.result_ = StreamResult("ACTIVE");
153
154   // Mark these as active ..
155   stats_collector_->StartStats(&connection_begin_stats_,
156                                &connection_end_stats_);
157   stats_collector_->StartStats(&stream_begin_stats_,
158                                &stream_end_stats_);
159
160   OpenMediaStats(media_request->serving_info().media_name_);
161   media_name_ = media_request->serving_info().media_name_;
162   if ( auth_helper_ != NULL &&
163        media_request_->auth_reply().reauthorize_interval_ms_ > 0 ) {
164     reauthorize_callback_ =
165         NewPermanentCallback(
166             this, &StreamRequest::Reauthorize);
167     selector_->RegisterAlarm(
168         reauthorize_callback_,
169         media_request_->auth_reply().reauthorize_interval_ms_);
170   }
171   if ( media_request_->auth_reply().time_limit_ms_ > 0 ) {
172     terminate_callback_ =
173         NewPermanentCallback(this, &StreamRequest::TerminateRequest);
174     selector_->RegisterAlarm(
175         terminate_callback_,
176         media_request_->auth_reply().time_limit_ms_);
177   }
178
179 }
180
181 void StreamRequest::ReauthorizeCallback() {
182   CHECK(auth_helper_ != NULL);
183
184   delete unpause_buffer_callback_;
185   unpause_buffer_callback_ = NULL;
186
187   *(media_request_->mutable_auth_reply()) = auth_helper_->reply();
188   if ( !auth_helper_->reply().allowed_ ) {
189     LOG_INFO << " Reauthorization failed for "
190              << media_request_->ToString();
191     ClearRequest(true);
192   } else {
193     if ( auth_helper_->reply().reauthorize_interval_ms_ > 0 ) {
194       if ( reauthorize_callback_ == NULL ) {
195         reauthorize_callback_ =
196             NewPermanentCallback(
197                 this, &StreamRequest::Reauthorize);
198
199       }
200       selector_->RegisterAlarm(reauthorize_callback_,
201                                auth_helper_->reply().reauthorize_interval_ms_);
202     }
203     if ( auth_helper_->reply().time_limit_ms_ > 0 ) {
204       if ( terminate_callback_ == NULL ) {
205         terminate_callback_ =
206             NewPermanentCallback(this, &StreamRequest::TerminateRequest);
207       }
208       selector_->ReregisterAlarm(terminate_callback_,
209                                 auth_helper_->reply().time_limit_ms_);
210     } else  if ( terminate_callback_ != NULL ) {
211       selector_->UnregisterAlarm(terminate_callback_);
212     }
213   }
214 }
215
216 void StreamRequest::Reauthorize() {
217   DCHECK(auth_helper_ != NULL);
218   auth_helper_->mutable_req()->action_performed_ms_ =
219       (selector_->now() - start_time_);
220   if ( auth_helper_->is_started() ) {
221     return;
222   }
223   auth_helper_->Start(NewCallback(this, &StreamRequest::ReauthorizeCallback));
224 }
225
226 void StreamRequest::TerminateRequest() {
227   LOG_INFO << " Authorization time ended for "
228            << media_request_->ToString();
229   ClearRequest(false);
230 }
231
232 StreamRequest::~StreamRequest() {
233   if ( reauthorize_callback_ != NULL ) {
234     selector_->UnregisterAlarm(reauthorize_callback_);
235     delete reauthorize_callback_;
236   }
237   if ( terminate_callback_ != NULL ) {
238     selector_->UnregisterAlarm(terminate_callback_);
239     delete terminate_callback_;
240   }
241   if ( auth_helper_ != NULL ) {
242     if ( auth_helper_->is_started() ) {
243       auth_helper_->Cancel();
244     } else {
245       delete auth_helper_;
246     }
247   }
248   // end media stats
249   CloseMediaStats();
250
251   const int64 now = timer::Date::Now();
252   // end stream stats
253   stream_end_stats_.timestamp_utc_ms_ = now;
254   if ( stream_end_stats_.result_.Get().result_.Get().StdStr() == "" ) {
255     stream_end_stats_.result_ = StreamResult("END");
256   }
257   stats_collector_->EndStats(&stream_end_stats_);
258
259   // end connection stats
260   connection_end_stats_.timestamp_utc_ms_ = now;
261   connection_end_stats_.result_ = ConnectionResult("END");
262   stats_collector_->EndStats(&connection_end_stats_);
263
264   // TODO[cpopescu] : record stats..
265   delete serializer_;
266   if ( media_request_ != NULL ) {
267     LOG_INFO << " Deleting Request for: " << media_request_->ToString();
268     mapper_->RemoveRequest(media_request_, processing_callback_);
269     media_request_ = NULL;
270   }
271   delete processing_callback_;
272   processing_callback_ = NULL;
273 }
274
275 bool StreamRequest::StartRequest() {
276   const char* media_name = media_request_->serving_info().media_name_.c_str();
277   *media_request_->mutable_caps() = mapper_->HasMedia(media_name);
278   switch ( media_request_->caps().tag_type_ ) {
279     case streaming::TYPE_MP3:
280       serializer_ = new streaming::Mp3TagSerializer();
281       break;
282     case streaming::TYPE_FLV:
283       serializer_ = new streaming::FlvTagSerializer();
284       break;
285     case streaming::TYPE_AAC:
286       serializer_ = new streaming::AacTagSerializer();
287       break;
288     case streaming::TYPE_INTERNAL_MEDIA:
289       serializer_ = new streaming::InternalTagSerializer();
290       break;
291     default:
292       serializer_ = new streaming::RawTagSerializer();
293       break;
294
295       LOG_ERROR << " Invalid serializer for element: " << media_name
296                 << " Tag type: "
297                 << TagTypeName(media_request_->caps().tag_type_);
298       serializer_ = NULL;
299   }
300   if ( serializer_ ) {
301     // Bootstrap the request
302     if ( mapper_->AddRequest(media_name,
303                              media_request_,
304                              processing_callback_) ) {
305       tag_type_ = media_request_->caps().tag_type_;
306       return true;
307     }
308     tag_type_ = streaming::INVALID_STREAM_TYPE;
309   }
310   delete media_request_;
311   media_request_ = NULL;
312   return false;
313 }
314
315 void StreamRequest::ClearRequest(bool from_server) {
316   LOG_INFO << " Clearing request: " << from_server;
317   CHECK(http_request_ != NULL);
318   http::ServerRequest* req = http_request_;
319   http_request_ = NULL;
320
321   stream_end_stats_.result_ = StreamResult(
322       ResultName(req->request()->server_header()->status_code()));
323   LOG_INFO << "Client ended: "
324            << "is_orphaned:" << req->is_orphaned();
325   // << " stats: " << stats_.ToString();
326
327   if ( !from_server ) {
328     if ( is_first_tag_ ) {
329       req->Reply();  // basically nothing was sent ..
330       selector_->DeleteInSelectLoop(this);
331     } else {
332       serializer_->Finalize(req->request()->server_data());
333       req->EndStreamingData();
334     }
335   }
336 }
337 void StreamRequest::EndOfStream() {
338   if ( http_request_ != NULL ) {
339     // If was closed before any returned tag
340     ClearRequest(true);
341   }
342   selector_->DeleteInSelectLoop(this);
343 }
344
345 void StreamRequest::ProcessTag(const streaming::Tag* tag,
346                                streaming::ElementController* controller) {
347   if ( tag->tag_type()  == streaming::TYPE_STREAM_EOS
348        || http_request_ == NULL
349        || http_request_->is_orphaned() ) {
350     if ( media_request_ != NULL ) {
351       mapper_->RemoveRequest(media_request_, processing_callback_);
352       media_request_ = NULL;
353     }
354     if ( http_request_ != NULL ) {
355       ClearRequest(false);
356     }
357     return;
358   }
359   if ( tag->tag_type() == streaming::TYPE_SOURCE_CHANGED ) {
360     CloseMediaStats();
361     OpenMediaStats(tag->data()->ToString());
362     media_name_ = tag->data()->ToString();
363     element_seq_id_++;
364   }
365   if ( tag->tag_type() == streaming::TYPE_SOURCE_CHANGED ||
366        tag->tag_type() == streaming::TYPE_SEEK_PERFORMED ||
367        tag->tag_type() == streaming::TYPE_SOURCE_RESTARTED ) {
368     is_first_tag_ = true;
369   }
370   // First tag to send - set content type and prepare the header..
371   const int64 now = timer::TicksMsec();
372   if ( is_first_tag_ ) {
373     first_tag_time_ = now;
374     is_first_tag_ = false;
375   }
376   if ( !http_request_->is_server_streaming() ) {
377     if ( media_request_->serving_info().enable_buffer_flow_control_ &&
378          streaming::IsRawTagType(media_request_->caps().tag_type_) &&
379          media_request_->serving_info().size_ > 0 ) {
380       http_request_->request()->server_header()->AddField(
381           http::kHeaderContentLength,
382           strutil::StringPrintf("%lld",
383                                 media_request_->serving_info().size_ -
384                                 media_request_->serving_info().offset_),
385           true);
386     }
387     http_request_->BeginStreamingData(
388         http::OK, NULL,
389         NewCallback(this, &StreamRequest::EndOfStream));
390     serializer_->Initialize(http_request_->request()->server_data());
391   }
392
393   // We may pause if we run over buffer size
394   if ( media_request_->serving_info().enable_buffer_flow_control_
395        && ( http_request_->free_output_bytes() - tag->size() <
396             (FLAGS_http_connection_max_media_outbuf_size >> 2) )
397        && unpause_buffer_callback_ == NULL
398        && controller != NULL
399        && controller->SupportsPause() ) {
400     controller->Pause(true);
401     unpause_buffer_callback_ = NewCallback(
402         this, &StreamRequest::UnpauseElementBuffer,
403         element_seq_id_, controller);
404   }
405
406   bool drop_tag = false;
407   // Update the stats..
408   if ( tag->is_stream_tag() ) {
409     if ( tag->is_audio_tag() ) {
410       // Possible resync and re-start syncing
411       // TODO: differentiate video / audio
412       if ( is_audio_dropping_
413            && http_request_->free_output_bytes() > tag->size() &&
414            tag->can_resync() ) {
415         is_audio_dropping_ = false;
416       } else if ( http_request_->free_output_bytes() < tag->size() &&
417                   tag->is_droppable() ) {
418         is_audio_dropping_ = true;
419       }
420
421       drop_tag |= is_audio_dropping_;
422       media_end_stats_.audio_frames_ = media_end_stats_.audio_frames_.Get() + 1;
423       if ( is_audio_dropping_ ) {
424         media_end_stats_.audio_frames_dropped_ =
425             media_end_stats_.audio_frames_dropped_.Get() + 1;
426       }
427     }
428     if ( tag->is_video_tag() ) {
429       // Possible resync and re-start sending for video
430       // TODO: differentiate video / audio
431       if ( is_video_dropping_
432            && http_request_->free_output_bytes() > tag->size() &&
433            tag->can_resync() ) {
434         is_video_dropping_ = false;
435       } else if ( http_request_->free_output_bytes() < tag->size() &&
436                   tag->is_droppable() ) {
437         is_video_dropping_ = true;
438       }
439       drop_tag |= is_video_dropping_;
440       media_end_stats_.video_frames_ =
441           media_end_stats_.video_frames_.Get() + 1;
442       if ( is_video_dropping_ ) {
443         media_end_stats_.video_frames_dropped_ =
444             media_end_stats_.video_frames_dropped_.Get() + 1;
445       }
446     }
447   }
448   // Send the data actually..
449   if ( !drop_tag ) {
450     const int32 initial_size = http_request_->request()->server_data()->Size();
451     last_tag_stream_time_ = tag->time_offset_ms();
452     const bool is_flv = tag->tag_type() == streaming::TYPE_FLV;
453     if ( is_flv ) {
454       const streaming::FlvTag* flv_tag =
455           static_cast<const streaming::FlvTag*>(tag->data());
456       if ( flv_tag->data_type() == streaming::FLV_FRAMETYPE_METADATA ) {
457         SerializeFlvMetadataTag(tag, flv_tag);
458       } else {
459         serializer_->Serialize(tag, http_request_->request()->server_data());
460       }
461     } else {
462       serializer_->Serialize(tag, http_request_->request()->server_data());
463     }
464     const int32 final_size = http_request_->request()->server_data()->Size();
465     connection_end_stats_.bytes_up_ = connection_end_stats_.bytes_up_.Get() +
466                                       final_size - initial_size;
467     media_end_stats_.bytes_up_ = media_end_stats_.bytes_up_.Get() +
468                                  final_size - initial_size;
469   }
470
471   // TODO: We may choose to do network flow control here..
472   http_request_->ContinueStreamingData(unpause_buffer_callback_);
473 }
474
475 void StreamRequest::UnpauseElementBuffer(
476     int64 element_seq_id,
477     streaming::ElementController* controller) {
478   DCHECK(unpause_buffer_callback_ != NULL);
479   DCHECK(controller->SupportsPause());
480   unpause_buffer_callback_ = NULL;
481   if ( element_seq_id != element_seq_id_ )
482     return;
483
484   controller->Pause(false);
485 }
486
487 void StreamRequest::SerializeFlvMetadataTag(
488     const streaming::Tag* tag,
489     const streaming::FlvTag* flv_tag) {
490   rtmp::CString name;
491   rtmp::CMixedMap values;
492   io::MemoryStream tag_content(common::BIGENDIAN);
493   tag_content.AppendStreamNonDestructive(flv_tag->data());
494   if ( (name.ReadFromMemoryStream(&tag_content,
495                                   rtmp::AmfUtil::AMF0_VERSION) !=
496         rtmp::AmfUtil::READ_OK)
497        || (values.ReadFromMemoryStream(&tag_content,
498                                        rtmp::AmfUtil::AMF0_VERSION) !=
499            rtmp::AmfUtil::READ_OK) ) {
500     serializer_->Serialize(tag, http_request_->request()->server_data());
501     return;
502   }
503   if ( name.value() == streaming::kOnMetaData ) {
504     if ( values.data().find("wc_mn") == values.data().end() ) {
505       values.mutable_data()["wc_mn"] =
506           new rtmp::CString(media_name_);
507     }
508     const rtmp::CMixedMap::Map::iterator
509         it_cue = values.mutable_data().find("cuePoints");
510     if ( it_cue != values.mutable_data().end() ) {
511       delete it_cue->second;
512       values.mutable_data().erase(it_cue);
513     }
514   } else if ( name.value() == streaming::kOnCuePoint ) {
515     if ( values.data().find("wc_mn") == values.data().end() ) {
516       values.mutable_data()["wc_mn"] = new rtmp::CString(media_name_);
517     }
518   } else {
519     serializer_->Serialize(tag, http_request_->request()->server_data());
520     return;
521   }
522   streaming::FlvTag* const copy_flv_tag =
523       reinterpret_cast<streaming::FlvTag*>(flv_tag->CopyClone());
524   copy_flv_tag->mutable_data()->Clear();
525   name.WriteToMemoryStream(copy_flv_tag->mutable_data(),
526                            rtmp::AmfUtil::AMF0_VERSION);
527   values.WriteToMemoryStream(copy_flv_tag->mutable_data(),
528                              rtmp::AmfUtil::AMF0_VERSION);
529   streaming::Tag* copy_tag = new streaming::Tag(
530       copy_flv_tag, copy_flv_tag->data()->Size(),
531       tag->tag_type(),
532       tag->data_attributes(),
533       tag->flavour_mask(),
534       tag->time_offset_ms());
535   serializer_->Serialize(copy_tag, http_request_->request()->server_data());
536   copy_tag->Release();
537
538 }
539 void StreamRequest::OpenMediaStats(const string& content_id) {
540   media_begin_stats_.media_id_ = strutil::StringPrintf("%s.%lld",
541           stream_begin_stats_.stream_id_.Get().CStr(),
542           static_cast<long long int>(media_id_));
543   ++media_id_;
544
545   const int64 now = timer::Date::Now();
546   media_begin_stats_.timestamp_utc_ms_ = now;
547   media_begin_stats_.stream_ = stream_begin_stats_;
548   media_begin_stats_.content_id_ = content_id;
549   const int64 time_delta = selector_->now() - start_time_;
550   media_begin_stats_.stream_time_ms_ = time_delta;
551   media_begin_stats_.content_time_ms_ = 0;
552
553   media_end_stats_.media_id_ = media_begin_stats_.media_id_;
554   media_end_stats_.timestamp_utc_ms_ = now;
555   media_end_stats_.bytes_up_ = 0;
556   media_end_stats_.bytes_down_ = 0;
557   media_end_stats_.video_frames_ = 0;
558   media_end_stats_.video_frames_dropped_ = 0;
559   media_end_stats_.audio_frames_ = 0;
560   media_end_stats_.audio_frames_dropped_ = 0;
561   media_end_stats_.result_ = MediaResult("Active");
562
563   LOG_INFO << "Seding media stats: " << media_begin_stats_;
564   stats_collector_->StartStats(&media_begin_stats_, &media_end_stats_);
565 }
566
567 void StreamRequest::CloseMediaStats() {
568   const int64 now = timer::Date::Now();
569   media_end_stats_.timestamp_utc_ms_ = now;
570   // TODO(cosmin): duration_in_ms_ should be content time! not physical time
571   const int64 time_delta = now -
572                            media_begin_stats_.stream_time_ms_.Get();
573   media_end_stats_.duration_in_ms_ = time_delta;
574   media_end_stats_.result_ = MediaResult("END");
575   LOG_INFO << "Ending media stats: " << media_end_stats_;
576   stats_collector_->EndStats(&media_end_stats_);
577 }
Note: See TracBrowser for help on using the browser.