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 "media/formats/mp2t/mp2t_stream_parser.h" | 5 #include "media/formats/mp2t/mp2t_stream_parser.h" |
6 | 6 |
7 #include <utility> | 7 #include <utility> |
8 | 8 |
9 #include "base/bind.h" | 9 #include "base/bind.h" |
10 #include "base/callback_helpers.h" | 10 #include "base/callback_helpers.h" |
11 #include "base/stl_util.h" | 11 #include "base/stl_util.h" |
12 #include "media/base/media_tracks.h" | 12 #include "media/base/media_tracks.h" |
13 #include "media/base/stream_parser_buffer.h" | 13 #include "media/base/stream_parser_buffer.h" |
14 #include "media/base/text_track_config.h" | 14 #include "media/base/text_track_config.h" |
15 #include "media/base/timestamp_constants.h" | 15 #include "media/base/timestamp_constants.h" |
16 #include "media/formats/mp2t/es_parser.h" | 16 #include "media/formats/mp2t/es_parser.h" |
17 #include "media/formats/mp2t/es_parser_adts.h" | 17 #include "media/formats/mp2t/es_parser_adts.h" |
18 #include "media/formats/mp2t/es_parser_h264.h" | 18 #include "media/formats/mp2t/es_parser_h264.h" |
19 #include "media/formats/mp2t/es_parser_mpeg1audio.h" | 19 #include "media/formats/mp2t/es_parser_mpeg1audio.h" |
20 #include "media/formats/mp2t/mp2t_common.h" | 20 #include "media/formats/mp2t/mp2t_common.h" |
21 #include "media/formats/mp2t/ts_packet.h" | 21 #include "media/formats/mp2t/ts_packet.h" |
22 #include "media/formats/mp2t/ts_section.h" | 22 #include "media/formats/mp2t/ts_section.h" |
23 #include "media/formats/mp2t/ts_section_pat.h" | 23 #include "media/formats/mp2t/ts_section_pat.h" |
24 #include "media/formats/mp2t/ts_section_pes.h" | 24 #include "media/formats/mp2t/ts_section_pes.h" |
25 #include "media/formats/mp2t/ts_section_pmt.h" | 25 #include "media/formats/mp2t/ts_section_pmt.h" |
26 | 26 |
| 27 #if BUILDFLAG(ENABLE_HLS_SAMPLE_AES) |
| 28 #include "media/formats/mp2t/ts_section_cat.h" |
| 29 #include "media/formats/mp2t/ts_section_cets_ecm.h" |
| 30 #include "media/formats/mp2t/ts_section_cets_pssh.h" |
| 31 #endif |
| 32 |
27 namespace media { | 33 namespace media { |
28 namespace mp2t { | 34 namespace mp2t { |
29 | 35 |
| 36 namespace { |
| 37 |
| 38 #if BUILDFLAG(ENABLE_HLS_SAMPLE_AES) |
| 39 const int64_t kSampleAESPrivateDataIndicatorAVC = 0x7a617663; |
| 40 const int64_t kSampleAESPrivateDataIndicatorAAC = 0x61616364; |
| 41 // TODO(dougsteed). Consider adding support for the following: |
| 42 // const int64_t kSampleAESPrivateDataIndicatorAC3 = 0x61633364; |
| 43 // const int64_t kSampleAESPrivateDataIndicatorEAC3 = 0x65633364; |
| 44 #endif |
| 45 |
| 46 } // namespace |
| 47 |
30 enum StreamType { | 48 enum StreamType { |
31 // ISO-13818.1 / ITU H.222 Table 2.34 "Stream type assignments" | 49 // ISO-13818.1 / ITU H.222 Table 2.34 "Stream type assignments" |
32 kStreamTypeMpeg1Audio = 0x3, | 50 kStreamTypeMpeg1Audio = 0x3, |
33 kStreamTypeAAC = 0xf, | 51 kStreamTypeAAC = 0xf, |
34 kStreamTypeAVC = 0x1b, | 52 kStreamTypeAVC = 0x1b, |
| 53 #if BUILDFLAG(ENABLE_HLS_SAMPLE_AES) |
| 54 kStreamTypeAACWithSampleAES = 0xcf, |
| 55 kStreamTypeAVCWithSampleAES = 0xdb, |
| 56 // TODO(dougsteed). Consider adding support for the following: |
| 57 // kStreamTypeAC3WithSampleAES = 0xc1, |
| 58 // kStreamTypeEAC3WithSampleAES = 0xc2, |
| 59 #endif |
35 }; | 60 }; |
36 | 61 |
37 class PidState { | 62 class PidState { |
38 public: | 63 public: |
39 enum PidType { | 64 enum PidType { |
40 kPidPat, | 65 kPidPat, |
41 kPidPmt, | 66 kPidPmt, |
42 kPidAudioPes, | 67 kPidAudioPes, |
43 kPidVideoPes, | 68 kPidVideoPes, |
| 69 #if BUILDFLAG(ENABLE_HLS_SAMPLE_AES) |
| 70 kPidCat, |
| 71 kPidCetsEcm, |
| 72 kPidCetsPssh, |
| 73 #endif |
44 }; | 74 }; |
45 | 75 |
46 PidState(int pid, PidType pid_tyoe, | 76 PidState(int pid, PidType pid_tyoe, |
47 scoped_ptr<TsSection> section_parser); | 77 scoped_ptr<TsSection> section_parser); |
48 | 78 |
49 // Extract the content of the TS packet and parse it. | 79 // Extract the content of the TS packet and parse it. |
50 // Return true if successful. | 80 // Return true if successful. |
51 bool PushTsPacket(const TsPacket& ts_packet); | 81 bool PushTsPacket(const TsPacket& ts_packet); |
52 | 82 |
53 // Flush the PID state (possibly emitting some pending frames) | 83 // Flush the PID state (possibly emitting some pending frames) |
(...skipping 136 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
190 encrypted_media_init_data_cb_ = encrypted_media_init_data_cb; | 220 encrypted_media_init_data_cb_ = encrypted_media_init_data_cb; |
191 new_segment_cb_ = new_segment_cb; | 221 new_segment_cb_ = new_segment_cb; |
192 end_of_segment_cb_ = end_of_segment_cb; | 222 end_of_segment_cb_ = end_of_segment_cb; |
193 media_log_ = media_log; | 223 media_log_ = media_log; |
194 } | 224 } |
195 | 225 |
196 void Mp2tStreamParser::Flush() { | 226 void Mp2tStreamParser::Flush() { |
197 DVLOG(1) << "Mp2tStreamParser::Flush"; | 227 DVLOG(1) << "Mp2tStreamParser::Flush"; |
198 | 228 |
199 // Flush the buffers and reset the pids. | 229 // Flush the buffers and reset the pids. |
200 for (std::map<int, PidState*>::iterator it = pids_.begin(); | 230 for (const auto& pid : pids_) { |
201 it != pids_.end(); ++it) { | 231 DVLOG(1) << "Flushing PID: " << pid.first; |
202 DVLOG(1) << "Flushing PID: " << it->first; | 232 PidState* pid_state = pid.second; |
203 PidState* pid_state = it->second; | |
204 pid_state->Flush(); | 233 pid_state->Flush(); |
205 delete pid_state; | 234 delete pid_state; |
206 } | 235 } |
207 pids_.clear(); | 236 pids_.clear(); |
208 | 237 |
209 // Flush is invoked from SourceBuffer.abort/SourceState::ResetParserState, and | 238 // Flush is invoked from SourceBuffer.abort/SourceState::ResetParserState, and |
210 // MSE spec prohibits emitting new configs in ResetParserState algorithm (see | 239 // MSE spec prohibits emitting new configs in ResetParserState algorithm (see |
211 // https://w3c.github.io/media-source/#sourcebuffer-reset-parser-state, | 240 // https://w3c.github.io/media-source/#sourcebuffer-reset-parser-state, |
212 // 3.5.2 Reset Parser State states that new frames might be processed only in | 241 // 3.5.2 Reset Parser State states that new frames might be processed only in |
213 // PARSING_MEDIA_SEGMENT and therefore doesn't allow emitting new configs, | 242 // PARSING_MEDIA_SEGMENT and therefore doesn't allow emitting new configs, |
(...skipping 57 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
271 if (!ts_packet) { | 300 if (!ts_packet) { |
272 DVLOG(1) << "Error: invalid TS packet"; | 301 DVLOG(1) << "Error: invalid TS packet"; |
273 ts_byte_queue_.Pop(1); | 302 ts_byte_queue_.Pop(1); |
274 continue; | 303 continue; |
275 } | 304 } |
276 DVLOG(LOG_LEVEL_TS) | 305 DVLOG(LOG_LEVEL_TS) |
277 << "Processing PID=" << ts_packet->pid() | 306 << "Processing PID=" << ts_packet->pid() |
278 << " start_unit=" << ts_packet->payload_unit_start_indicator(); | 307 << " start_unit=" << ts_packet->payload_unit_start_indicator(); |
279 | 308 |
280 // Parse the section. | 309 // Parse the section. |
281 std::map<int, PidState*>::iterator it = pids_.find(ts_packet->pid()); | 310 auto it = pids_.find(ts_packet->pid()); |
282 if (it == pids_.end() && | 311 if (it == pids_.end() && |
283 ts_packet->pid() == TsSection::kPidPat) { | 312 ts_packet->pid() == TsSection::kPidPat) { |
284 // Create the PAT state here if needed. | 313 // Create the PAT state here if needed. |
285 scoped_ptr<TsSection> pat_section_parser( | 314 scoped_ptr<TsSection> pat_section_parser( |
286 new TsSectionPat( | 315 new TsSectionPat( |
287 base::Bind(&Mp2tStreamParser::RegisterPmt, | 316 base::Bind(&Mp2tStreamParser::RegisterPmt, |
288 base::Unretained(this)))); | 317 base::Unretained(this)))); |
289 scoped_ptr<PidState> pat_pid_state(new PidState( | 318 scoped_ptr<PidState> pat_pid_state(new PidState( |
290 ts_packet->pid(), PidState::kPidPat, std::move(pat_section_parser))); | 319 ts_packet->pid(), PidState::kPidPat, std::move(pat_section_parser))); |
291 pat_pid_state->Enable(); | 320 pat_pid_state->Enable(); |
292 it = pids_.insert( | 321 it = |
293 std::pair<int, PidState*>(ts_packet->pid(), | 322 pids_.insert(PidMapElement(ts_packet->pid(), pat_pid_state.release())) |
294 pat_pid_state.release())).first; | 323 .first; |
295 } | 324 } |
| 325 #if BUILDFLAG(ENABLE_HLS_SAMPLE_AES) |
| 326 // We allow a CAT to appear as the first packet in the TS. This allows us to |
| 327 // specify encryption metadata for HLS by injecting it as an extra TS packet |
| 328 // at the front of the stream. |
| 329 else if (it == pids_.end() && ts_packet->pid() == TsSection::kPidCat) { |
| 330 it = RegisterCat(); |
| 331 } |
| 332 #endif |
296 | 333 |
297 if (it != pids_.end()) { | 334 if (it != pids_.end()) { |
298 if (!it->second->PushTsPacket(*ts_packet)) | 335 if (!it->second->PushTsPacket(*ts_packet)) |
299 return false; | 336 return false; |
300 } else { | 337 } else { |
301 DVLOG(LOG_LEVEL_TS) << "Ignoring TS packet for pid: " << ts_packet->pid(); | 338 DVLOG(LOG_LEVEL_TS) << "Ignoring TS packet for pid: " << ts_packet->pid(); |
302 } | 339 } |
303 | 340 |
304 // Go to the next packet. | 341 // Go to the next packet. |
305 ts_byte_queue_.Pop(TsPacket::kPacketSize); | 342 ts_byte_queue_.Pop(TsPacket::kPacketSize); |
306 } | 343 } |
307 | 344 |
308 RCHECK(FinishInitializationIfNeeded()); | 345 RCHECK(FinishInitializationIfNeeded()); |
309 | 346 |
310 // Emit the A/V buffers that kept accumulating during TS parsing. | 347 // Emit the A/V buffers that kept accumulating during TS parsing. |
311 return EmitRemainingBuffers(); | 348 return EmitRemainingBuffers(); |
312 } | 349 } |
313 | 350 |
314 void Mp2tStreamParser::RegisterPmt(int program_number, int pmt_pid) { | 351 void Mp2tStreamParser::RegisterPmt(int program_number, int pmt_pid) { |
315 DVLOG(1) << "RegisterPmt:" | 352 DVLOG(1) << "RegisterPmt:" |
316 << " program_number=" << program_number | 353 << " program_number=" << program_number |
317 << " pmt_pid=" << pmt_pid; | 354 << " pmt_pid=" << pmt_pid; |
318 | 355 |
319 // Only one TS program is allowed. Ignore the incoming program map table, | 356 // Only one TS program is allowed. Ignore the incoming program map table, |
320 // if there is already one registered. | 357 // if there is already one registered. |
321 for (std::map<int, PidState*>::iterator it = pids_.begin(); | 358 for (const auto& pid : pids_) { |
322 it != pids_.end(); ++it) { | 359 PidState* pid_state = pid.second; |
323 PidState* pid_state = it->second; | |
324 if (pid_state->pid_type() == PidState::kPidPmt) { | 360 if (pid_state->pid_type() == PidState::kPidPmt) { |
325 DVLOG_IF(1, pmt_pid != it->first) << "More than one program is defined"; | 361 DVLOG_IF(1, pmt_pid != pid.first) << "More than one program is defined"; |
326 return; | 362 return; |
327 } | 363 } |
328 } | 364 } |
329 | 365 |
330 // Create the PMT state here if needed. | 366 // Create the PMT state here if needed. |
331 DVLOG(1) << "Create a new PMT parser"; | 367 DVLOG(1) << "Create a new PMT parser"; |
332 scoped_ptr<TsSection> pmt_section_parser( | 368 scoped_ptr<TsSection> pmt_section_parser( |
333 new TsSectionPmt( | 369 new TsSectionPmt( |
334 base::Bind(&Mp2tStreamParser::RegisterPes, | 370 base::Bind(&Mp2tStreamParser::RegisterPes, |
335 base::Unretained(this), pmt_pid))); | 371 base::Unretained(this), pmt_pid))); |
336 scoped_ptr<PidState> pmt_pid_state( | 372 scoped_ptr<PidState> pmt_pid_state( |
337 new PidState(pmt_pid, PidState::kPidPmt, std::move(pmt_section_parser))); | 373 new PidState(pmt_pid, PidState::kPidPmt, std::move(pmt_section_parser))); |
338 pmt_pid_state->Enable(); | 374 pmt_pid_state->Enable(); |
339 pids_.insert(std::pair<int, PidState*>(pmt_pid, pmt_pid_state.release())); | 375 pids_.insert(std::pair<int, PidState*>(pmt_pid, pmt_pid_state.release())); |
| 376 |
| 377 #if BUILDFLAG(ENABLE_HLS_SAMPLE_AES) |
| 378 // Take the opportunity to clean up any PIDs that were involved in importing |
| 379 // encryption metadata for HLS with SampleAES. This prevents the possibility |
| 380 // of interference with actual PIDs that might be declared in the PMT. |
| 381 // TODO(dougsteed): if in the future the appropriate PIDs are embedded in the |
| 382 // source stream, this will not be necessary. |
| 383 UnregisterCat(); |
| 384 UnregisterCencPids(); |
| 385 #endif |
340 } | 386 } |
341 | 387 |
342 void Mp2tStreamParser::RegisterPes(int pmt_pid, | 388 void Mp2tStreamParser::RegisterPes(int pmt_pid, |
343 int pes_pid, | 389 int pes_pid, |
344 int stream_type) { | 390 int stream_type, |
| 391 const Descriptors& descriptors) { |
345 // TODO(damienv): check there is no mismatch if the entry already exists. | 392 // TODO(damienv): check there is no mismatch if the entry already exists. |
346 DVLOG(1) << "RegisterPes:" | 393 DVLOG(1) << "RegisterPes:" |
347 << " pes_pid=" << pes_pid | 394 << " pes_pid=" << pes_pid |
348 << " stream_type=" << std::hex << stream_type << std::dec; | 395 << " stream_type=" << std::hex << stream_type << std::dec; |
349 std::map<int, PidState*>::iterator it = pids_.find(pes_pid); | 396 std::map<int, PidState*>::iterator it = pids_.find(pes_pid); |
350 if (it != pids_.end()) | 397 if (it != pids_.end()) |
351 return; | 398 return; |
352 | 399 |
353 // Create a stream parser corresponding to the stream type. | 400 // Create a stream parser corresponding to the stream type. |
354 bool is_audio = false; | 401 bool is_audio = false; |
(...skipping 19 matching lines...) Expand all Loading... |
374 sbr_in_mimetype_)); | 421 sbr_in_mimetype_)); |
375 is_audio = true; | 422 is_audio = true; |
376 } else if (stream_type == kStreamTypeMpeg1Audio) { | 423 } else if (stream_type == kStreamTypeMpeg1Audio) { |
377 es_parser.reset(new EsParserMpeg1Audio( | 424 es_parser.reset(new EsParserMpeg1Audio( |
378 base::Bind(&Mp2tStreamParser::OnAudioConfigChanged, | 425 base::Bind(&Mp2tStreamParser::OnAudioConfigChanged, |
379 base::Unretained(this), pes_pid), | 426 base::Unretained(this), pes_pid), |
380 base::Bind(&Mp2tStreamParser::OnEmitAudioBuffer, base::Unretained(this), | 427 base::Bind(&Mp2tStreamParser::OnEmitAudioBuffer, base::Unretained(this), |
381 pes_pid), | 428 pes_pid), |
382 media_log_)); | 429 media_log_)); |
383 is_audio = true; | 430 is_audio = true; |
| 431 #if BUILDFLAG(ENABLE_HLS_SAMPLE_AES) |
| 432 } else if (stream_type == kStreamTypeAVCWithSampleAES && |
| 433 descriptors.HasPrivateDataIndicator( |
| 434 kSampleAESPrivateDataIndicatorAVC)) { |
| 435 es_parser.reset(new EsParserH264( |
| 436 base::Bind(&Mp2tStreamParser::OnVideoConfigChanged, |
| 437 base::Unretained(this), pes_pid), |
| 438 base::Bind(&Mp2tStreamParser::OnEmitVideoBuffer, base::Unretained(this), |
| 439 pes_pid), |
| 440 base::Bind(&Mp2tStreamParser::GetDecryptConfig, base::Unretained(this)), |
| 441 true)); |
| 442 } else if (stream_type == kStreamTypeAACWithSampleAES && |
| 443 descriptors.HasPrivateDataIndicator( |
| 444 kSampleAESPrivateDataIndicatorAAC)) { |
| 445 es_parser.reset(new EsParserAdts( |
| 446 base::Bind(&Mp2tStreamParser::OnAudioConfigChanged, |
| 447 base::Unretained(this), pes_pid), |
| 448 base::Bind(&Mp2tStreamParser::OnEmitAudioBuffer, base::Unretained(this), |
| 449 pes_pid), |
| 450 base::Bind(&Mp2tStreamParser::GetDecryptConfig, base::Unretained(this)), |
| 451 true, sbr_in_mimetype_)); |
| 452 is_audio = true; |
| 453 #endif |
384 } else { | 454 } else { |
385 return; | 455 return; |
386 } | 456 } |
387 | 457 |
388 // Create the PES state here. | 458 // Create the PES state here. |
389 DVLOG(1) << "Create a new PES state"; | 459 DVLOG(1) << "Create a new PES state"; |
390 scoped_ptr<TsSection> pes_section_parser( | 460 scoped_ptr<TsSection> pes_section_parser( |
391 new TsSectionPes(std::move(es_parser), ×tamp_unroller_)); | 461 new TsSectionPes(std::move(es_parser), ×tamp_unroller_)); |
392 PidState::PidType pid_type = | 462 PidState::PidType pid_type = |
393 is_audio ? PidState::kPidAudioPes : PidState::kPidVideoPes; | 463 is_audio ? PidState::kPidAudioPes : PidState::kPidVideoPes; |
(...skipping 265 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
659 | 729 |
660 // Push an empty queue with the last audio/video config | 730 // Push an empty queue with the last audio/video config |
661 // so that buffers with the same config can be added later on. | 731 // so that buffers with the same config can be added later on. |
662 BufferQueueWithConfig queue_with_config( | 732 BufferQueueWithConfig queue_with_config( |
663 true, last_audio_config, last_video_config); | 733 true, last_audio_config, last_video_config); |
664 buffer_queue_chain_.push_back(queue_with_config); | 734 buffer_queue_chain_.push_back(queue_with_config); |
665 | 735 |
666 return true; | 736 return true; |
667 } | 737 } |
668 | 738 |
| 739 #if BUILDFLAG(ENABLE_HLS_SAMPLE_AES) |
| 740 std::map<int, PidState*>::iterator Mp2tStreamParser::RegisterCat() { |
| 741 scoped_ptr<TsSection> cat_section_parser(new TsSectionCat( |
| 742 base::Bind(&Mp2tStreamParser::RegisterCencPids, base::Unretained(this)))); |
| 743 scoped_ptr<PidState> cat_pid_state(new PidState( |
| 744 TsSection::kPidCat, PidState::kPidCat, std::move(cat_section_parser))); |
| 745 cat_pid_state->Enable(); |
| 746 return pids_ |
| 747 .insert(PidMapElement(TsSection::kPidCat, cat_pid_state.release())) |
| 748 .first; |
| 749 } |
| 750 |
| 751 void Mp2tStreamParser::UnregisterCat() { |
| 752 for (auto& pid : pids_) { |
| 753 if (pid.second->pid_type() == PidState::kPidCat) { |
| 754 delete pid.second; |
| 755 pids_.erase(pid.first); |
| 756 break; |
| 757 } |
| 758 } |
| 759 } |
| 760 |
| 761 void Mp2tStreamParser::RegisterCencPids(int ca_pid, int pssh_pid) { |
| 762 scoped_ptr<TsSectionCetsEcm> ecm_parser(new TsSectionCetsEcm(base::Bind( |
| 763 &Mp2tStreamParser::RegisterDecryptConfig, base::Unretained(this)))); |
| 764 scoped_ptr<PidState> ecm_pid_state( |
| 765 new PidState(ca_pid, PidState::kPidCetsEcm, std::move(ecm_parser))); |
| 766 ecm_pid_state->Enable(); |
| 767 pids_.insert(PidMapElement(ca_pid, ecm_pid_state.release())); |
| 768 |
| 769 scoped_ptr<TsSectionCetsPssh> pssh_parser(new TsSectionCetsPssh(base::Bind( |
| 770 &Mp2tStreamParser::RegisterPsshBoxes, base::Unretained(this)))); |
| 771 scoped_ptr<PidState> pssh_pid_state( |
| 772 new PidState(pssh_pid, PidState::kPidCetsPssh, std::move(pssh_parser))); |
| 773 pssh_pid_state->Enable(); |
| 774 pids_.insert(PidMapElement(pssh_pid, pssh_pid_state.release())); |
| 775 } |
| 776 |
| 777 void Mp2tStreamParser::UnregisterCencPids() { |
| 778 for (auto& pid : pids_) { |
| 779 if (pid.second->pid_type() == PidState::kPidCetsEcm) { |
| 780 delete pid.second; |
| 781 pids_.erase(pid.first); |
| 782 break; |
| 783 } |
| 784 } |
| 785 for (auto& pid : pids_) { |
| 786 if (pid.second->pid_type() == PidState::kPidCetsPssh) { |
| 787 delete pid.second; |
| 788 pids_.erase(pid.first); |
| 789 break; |
| 790 } |
| 791 } |
| 792 } |
| 793 |
| 794 void Mp2tStreamParser::RegisterDecryptConfig(const DecryptConfig& config) { |
| 795 decrypt_config_.reset( |
| 796 new DecryptConfig(config.key_id(), config.iv(), config.subsamples())); |
| 797 } |
| 798 |
| 799 void Mp2tStreamParser::RegisterPsshBoxes( |
| 800 const std::vector<uint8_t>& init_data) { |
| 801 encrypted_media_init_data_cb_.Run(EmeInitDataType::CENC, init_data); |
| 802 } |
| 803 |
| 804 #endif |
| 805 |
669 } // namespace mp2t | 806 } // namespace mp2t |
670 } // namespace media | 807 } // namespace media |
OLD | NEW |