Benjamin Wright | 87bbb91 | 2019-02-01 18:00:05 | [diff] [blame] | 1 | /* |
| 2 | * Copyright (c) 2019 The WebRTC project authors. All Rights Reserved. |
| 3 | * |
| 4 | * Use of this source code is governed by a BSD-style license |
| 5 | * that can be found in the LICENSE file in the root of the source |
| 6 | * tree. An additional intellectual property rights grant can be found |
| 7 | * in the file PATENTS. All contributing project authors may |
| 8 | * be found in the AUTHORS file in the root of the source tree. |
| 9 | */ |
| 10 | |
| 11 | #include "rtc_tools/rtp_generator/rtp_generator.h" |
| 12 | |
| 13 | #include <algorithm> |
Mirko Bonadei | 317a1f0 | 2019-09-17 15:06:18 | [diff] [blame] | 14 | #include <memory> |
Benjamin Wright | 87bbb91 | 2019-02-01 18:00:05 | [diff] [blame] | 15 | #include <utility> |
| 16 | |
Danil Chapovalov | 702820d | 2023-11-28 17:25:28 | [diff] [blame] | 17 | #include "api/environment/environment_factory.h" |
Artem Titov | 33f9d2b | 2019-12-05 14:59:00 | [diff] [blame] | 18 | #include "api/test/create_frame_generator.h" |
Ying Wang | bc959b1 | 2023-04-03 02:54:31 | [diff] [blame] | 19 | #include "api/video_codecs/video_decoder_factory_template.h" |
| 20 | #include "api/video_codecs/video_decoder_factory_template_dav1d_adapter.h" |
| 21 | #include "api/video_codecs/video_decoder_factory_template_libvpx_vp8_adapter.h" |
| 22 | #include "api/video_codecs/video_decoder_factory_template_libvpx_vp9_adapter.h" |
Benjamin Wright | 87bbb91 | 2019-02-01 18:00:05 | [diff] [blame] | 23 | #include "api/video_codecs/video_encoder.h" |
Ying Wang | bc959b1 | 2023-04-03 02:54:31 | [diff] [blame] | 24 | #include "api/video_codecs/video_encoder_factory.h" |
| 25 | #include "api/video_codecs/video_encoder_factory_template.h" |
| 26 | #include "api/video_codecs/video_encoder_factory_template_libaom_av1_adapter.h" |
| 27 | #include "api/video_codecs/video_encoder_factory_template_libvpx_vp8_adapter.h" |
| 28 | #include "api/video_codecs/video_encoder_factory_template_libvpx_vp9_adapter.h" |
Benjamin Wright | 87bbb91 | 2019-02-01 18:00:05 | [diff] [blame] | 29 | #include "media/base/media_constants.h" |
| 30 | #include "rtc_base/strings/json.h" |
| 31 | #include "rtc_base/system/file_wrapper.h" |
| 32 | #include "rtc_base/thread.h" |
| 33 | #include "test/testsupport/file_utils.h" |
Jonas Oreland | 1262eb5 | 2022-09-27 14:53:04 | [diff] [blame] | 34 | #include "video/config/encoder_stream_factory.h" |
Jonas Oreland | 6c2dae2 | 2022-09-29 08:28:24 | [diff] [blame] | 35 | #include "video/config/video_encoder_config.h" |
Benjamin Wright | 87bbb91 | 2019-02-01 18:00:05 | [diff] [blame] | 36 | |
| 37 | namespace webrtc { |
| 38 | namespace { |
| 39 | |
| 40 | // Payload types. |
| 41 | constexpr int kPayloadTypeVp8 = 125; |
| 42 | constexpr int kPayloadTypeVp9 = 124; |
| 43 | constexpr int kPayloadTypeH264 = 123; |
| 44 | constexpr int kFakeVideoSendPayloadType = 122; |
| 45 | |
| 46 | // Defaults |
| 47 | constexpr int kDefaultSsrc = 1337; |
| 48 | constexpr int kMaxConfigBufferSize = 8192; |
| 49 | |
| 50 | // Utility function to validate a correct codec type has been passed in. |
| 51 | bool IsValidCodecType(const std::string& codec_name) { |
| 52 | return cricket::kVp8CodecName == codec_name || |
| 53 | cricket::kVp9CodecName == codec_name || |
| 54 | cricket::kH264CodecName == codec_name; |
| 55 | } |
| 56 | |
| 57 | // Utility function to return some base payload type for a codec_name. |
| 58 | int GetDefaultTypeForPayloadName(const std::string& codec_name) { |
| 59 | if (cricket::kVp8CodecName == codec_name) { |
| 60 | return kPayloadTypeVp8; |
| 61 | } |
| 62 | if (cricket::kVp9CodecName == codec_name) { |
| 63 | return kPayloadTypeVp9; |
| 64 | } |
| 65 | if (cricket::kH264CodecName == codec_name) { |
| 66 | return kPayloadTypeH264; |
| 67 | } |
| 68 | return kFakeVideoSendPayloadType; |
| 69 | } |
| 70 | |
| 71 | // Creates a single VideoSendStream configuration. |
Florent Castelli | 8037fc6 | 2024-08-29 13:00:40 | [diff] [blame] | 72 | std::optional<RtpGeneratorOptions::VideoSendStreamConfig> |
Benjamin Wright | 87bbb91 | 2019-02-01 18:00:05 | [diff] [blame] | 73 | ParseVideoSendStreamConfig(const Json::Value& json) { |
| 74 | RtpGeneratorOptions::VideoSendStreamConfig config; |
| 75 | |
| 76 | // Parse video source settings. |
| 77 | if (!rtc::GetIntFromJsonObject(json, "duration_ms", &config.duration_ms)) { |
| 78 | RTC_LOG(LS_WARNING) << "duration_ms not specified using default: " |
| 79 | << config.duration_ms; |
| 80 | } |
| 81 | if (!rtc::GetIntFromJsonObject(json, "video_width", &config.video_width)) { |
| 82 | RTC_LOG(LS_WARNING) << "video_width not specified using default: " |
| 83 | << config.video_width; |
| 84 | } |
| 85 | if (!rtc::GetIntFromJsonObject(json, "video_height", &config.video_height)) { |
| 86 | RTC_LOG(LS_WARNING) << "video_height not specified using default: " |
| 87 | << config.video_height; |
| 88 | } |
| 89 | if (!rtc::GetIntFromJsonObject(json, "video_fps", &config.video_fps)) { |
| 90 | RTC_LOG(LS_WARNING) << "video_fps not specified using default: " |
| 91 | << config.video_fps; |
| 92 | } |
| 93 | if (!rtc::GetIntFromJsonObject(json, "num_squares", &config.num_squares)) { |
| 94 | RTC_LOG(LS_WARNING) << "num_squares not specified using default: " |
| 95 | << config.num_squares; |
| 96 | } |
| 97 | |
| 98 | // Parse RTP settings for this configuration. |
| 99 | config.rtp.ssrcs.push_back(kDefaultSsrc); |
| 100 | Json::Value rtp_json; |
| 101 | if (!rtc::GetValueFromJsonObject(json, "rtp", &rtp_json)) { |
| 102 | RTC_LOG(LS_ERROR) << "video_streams must have an rtp section"; |
Florent Castelli | 8037fc6 | 2024-08-29 13:00:40 | [diff] [blame] | 103 | return std::nullopt; |
Benjamin Wright | 87bbb91 | 2019-02-01 18:00:05 | [diff] [blame] | 104 | } |
| 105 | if (!rtc::GetStringFromJsonObject(rtp_json, "payload_name", |
| 106 | &config.rtp.payload_name)) { |
| 107 | RTC_LOG(LS_ERROR) << "rtp.payload_name must be specified"; |
Florent Castelli | 8037fc6 | 2024-08-29 13:00:40 | [diff] [blame] | 108 | return std::nullopt; |
Benjamin Wright | 87bbb91 | 2019-02-01 18:00:05 | [diff] [blame] | 109 | } |
| 110 | if (!IsValidCodecType(config.rtp.payload_name)) { |
| 111 | RTC_LOG(LS_ERROR) << "rtp.payload_name must be VP8,VP9 or H264"; |
Florent Castelli | 8037fc6 | 2024-08-29 13:00:40 | [diff] [blame] | 112 | return std::nullopt; |
Benjamin Wright | 87bbb91 | 2019-02-01 18:00:05 | [diff] [blame] | 113 | } |
| 114 | |
| 115 | config.rtp.payload_type = |
| 116 | GetDefaultTypeForPayloadName(config.rtp.payload_name); |
| 117 | if (!rtc::GetIntFromJsonObject(rtp_json, "payload_type", |
| 118 | &config.rtp.payload_type)) { |
| 119 | RTC_LOG(LS_WARNING) |
| 120 | << "rtp.payload_type not specified using default for codec type" |
| 121 | << config.rtp.payload_type; |
| 122 | } |
| 123 | |
| 124 | return config; |
| 125 | } |
| 126 | |
| 127 | } // namespace |
| 128 | |
Florent Castelli | 8037fc6 | 2024-08-29 13:00:40 | [diff] [blame] | 129 | std::optional<RtpGeneratorOptions> ParseRtpGeneratorOptionsFromFile( |
Benjamin Wright | 87bbb91 | 2019-02-01 18:00:05 | [diff] [blame] | 130 | const std::string& options_file) { |
| 131 | if (!test::FileExists(options_file)) { |
| 132 | RTC_LOG(LS_ERROR) << " configuration file does not exist"; |
Florent Castelli | 8037fc6 | 2024-08-29 13:00:40 | [diff] [blame] | 133 | return std::nullopt; |
Benjamin Wright | 87bbb91 | 2019-02-01 18:00:05 | [diff] [blame] | 134 | } |
| 135 | |
| 136 | // Read the configuration file from disk. |
| 137 | FileWrapper config_file = FileWrapper::OpenReadOnly(options_file); |
| 138 | std::vector<char> raw_json_buffer(kMaxConfigBufferSize, 0); |
| 139 | size_t bytes_read = |
| 140 | config_file.Read(raw_json_buffer.data(), raw_json_buffer.size() - 1); |
| 141 | if (bytes_read == 0) { |
| 142 | RTC_LOG(LS_ERROR) << "Unable to read the configuration file."; |
Florent Castelli | 8037fc6 | 2024-08-29 13:00:40 | [diff] [blame] | 143 | return std::nullopt; |
Benjamin Wright | 87bbb91 | 2019-02-01 18:00:05 | [diff] [blame] | 144 | } |
| 145 | |
| 146 | // Parse the file as JSON |
Mirko Bonadei | e99f687 | 2021-06-24 13:24:59 | [diff] [blame] | 147 | Json::CharReaderBuilder builder; |
Benjamin Wright | 87bbb91 | 2019-02-01 18:00:05 | [diff] [blame] | 148 | Json::Value json; |
Mirko Bonadei | e99f687 | 2021-06-24 13:24:59 | [diff] [blame] | 149 | std::string error_message; |
| 150 | std::unique_ptr<Json::CharReader> json_reader(builder.newCharReader()); |
| 151 | if (!json_reader->parse(raw_json_buffer.data(), |
| 152 | raw_json_buffer.data() + raw_json_buffer.size(), |
| 153 | &json, &error_message)) { |
| 154 | RTC_LOG(LS_ERROR) << "Unable to parse the corpus config json file. Error:" |
| 155 | << error_message; |
Florent Castelli | 8037fc6 | 2024-08-29 13:00:40 | [diff] [blame] | 156 | return std::nullopt; |
Benjamin Wright | 87bbb91 | 2019-02-01 18:00:05 | [diff] [blame] | 157 | } |
| 158 | |
| 159 | RtpGeneratorOptions gen_options; |
| 160 | for (const auto& video_stream_json : json["video_streams"]) { |
Florent Castelli | 8037fc6 | 2024-08-29 13:00:40 | [diff] [blame] | 161 | std::optional<RtpGeneratorOptions::VideoSendStreamConfig> |
Benjamin Wright | 87bbb91 | 2019-02-01 18:00:05 | [diff] [blame] | 162 | video_stream_config = ParseVideoSendStreamConfig(video_stream_json); |
| 163 | if (!video_stream_config.has_value()) { |
| 164 | RTC_LOG(LS_ERROR) << "Unable to parse the corpus config json file"; |
Florent Castelli | 8037fc6 | 2024-08-29 13:00:40 | [diff] [blame] | 165 | return std::nullopt; |
Benjamin Wright | 87bbb91 | 2019-02-01 18:00:05 | [diff] [blame] | 166 | } |
| 167 | gen_options.video_streams.push_back(*video_stream_config); |
| 168 | } |
| 169 | return gen_options; |
| 170 | } |
| 171 | |
| 172 | RtpGenerator::RtpGenerator(const RtpGeneratorOptions& options) |
| 173 | : options_(options), |
Danil Chapovalov | 702820d | 2023-11-28 17:25:28 | [diff] [blame] | 174 | env_(CreateEnvironment()), |
Ying Wang | bc959b1 | 2023-04-03 02:54:31 | [diff] [blame] | 175 | video_encoder_factory_( |
| 176 | std::make_unique<webrtc::VideoEncoderFactoryTemplate< |
| 177 | webrtc::LibvpxVp8EncoderTemplateAdapter, |
| 178 | webrtc::LibvpxVp9EncoderTemplateAdapter, |
| 179 | webrtc::LibaomAv1EncoderTemplateAdapter>>()), |
| 180 | video_decoder_factory_( |
| 181 | std::make_unique<webrtc::VideoDecoderFactoryTemplate< |
| 182 | webrtc::LibvpxVp8DecoderTemplateAdapter, |
| 183 | webrtc::LibvpxVp9DecoderTemplateAdapter, |
| 184 | webrtc::Dav1dDecoderTemplateAdapter>>()), |
Benjamin Wright | 87bbb91 | 2019-02-01 18:00:05 | [diff] [blame] | 185 | video_bitrate_allocator_factory_( |
| 186 | CreateBuiltinVideoBitrateAllocatorFactory()), |
Danil Chapovalov | 702820d | 2023-11-28 17:25:28 | [diff] [blame] | 187 | call_(Call::Create(CallConfig(env_))) { |
Benjamin Wright | 87bbb91 | 2019-02-01 18:00:05 | [diff] [blame] | 188 | constexpr int kMinBitrateBps = 30000; // 30 Kbps |
| 189 | constexpr int kMaxBitrateBps = 2500000; // 2.5 Mbps |
| 190 | |
| 191 | int stream_count = 0; |
Jonas Oreland | 80c87d7 | 2022-09-29 13:01:09 | [diff] [blame] | 192 | webrtc::VideoEncoder::EncoderInfo encoder_info; |
Benjamin Wright | 87bbb91 | 2019-02-01 18:00:05 | [diff] [blame] | 193 | for (const auto& send_config : options.video_streams) { |
| 194 | webrtc::VideoSendStream::Config video_config(this); |
| 195 | video_config.encoder_settings.encoder_factory = |
| 196 | video_encoder_factory_.get(); |
| 197 | video_config.encoder_settings.bitrate_allocator_factory = |
| 198 | video_bitrate_allocator_factory_.get(); |
| 199 | video_config.rtp = send_config.rtp; |
| 200 | // Update some required to be unique values. |
| 201 | stream_count++; |
| 202 | video_config.rtp.mid = "mid-" + std::to_string(stream_count); |
Benjamin Wright | 87bbb91 | 2019-02-01 18:00:05 | [diff] [blame] | 203 | |
| 204 | // Configure the video encoder configuration. |
| 205 | VideoEncoderConfig encoder_config; |
| 206 | encoder_config.content_type = |
| 207 | VideoEncoderConfig::ContentType::kRealtimeVideo; |
| 208 | encoder_config.codec_type = |
| 209 | PayloadStringToCodecType(video_config.rtp.payload_name); |
| 210 | if (video_config.rtp.payload_name == cricket::kVp8CodecName) { |
| 211 | VideoCodecVP8 settings = VideoEncoder::GetDefaultVp8Settings(); |
Tommi | 87f7090 | 2021-04-27 12:43:08 | [diff] [blame] | 212 | encoder_config.encoder_specific_settings = |
| 213 | rtc::make_ref_counted<VideoEncoderConfig::Vp8EncoderSpecificSettings>( |
| 214 | settings); |
Benjamin Wright | 87bbb91 | 2019-02-01 18:00:05 | [diff] [blame] | 215 | } else if (video_config.rtp.payload_name == cricket::kVp9CodecName) { |
| 216 | VideoCodecVP9 settings = VideoEncoder::GetDefaultVp9Settings(); |
Tommi | 87f7090 | 2021-04-27 12:43:08 | [diff] [blame] | 217 | encoder_config.encoder_specific_settings = |
| 218 | rtc::make_ref_counted<VideoEncoderConfig::Vp9EncoderSpecificSettings>( |
| 219 | settings); |
Benjamin Wright | 87bbb91 | 2019-02-01 18:00:05 | [diff] [blame] | 220 | } else if (video_config.rtp.payload_name == cricket::kH264CodecName) { |
Niels Möller | cf2c891 | 2022-05-18 08:45:46 | [diff] [blame] | 221 | encoder_config.encoder_specific_settings = nullptr; |
Benjamin Wright | 87bbb91 | 2019-02-01 18:00:05 | [diff] [blame] | 222 | } |
| 223 | encoder_config.video_format.name = video_config.rtp.payload_name; |
| 224 | encoder_config.min_transmit_bitrate_bps = 0; |
| 225 | encoder_config.max_bitrate_bps = kMaxBitrateBps; |
| 226 | encoder_config.content_type = |
| 227 | VideoEncoderConfig::ContentType::kRealtimeVideo; |
| 228 | |
| 229 | // Configure the simulcast layers. |
| 230 | encoder_config.number_of_streams = video_config.rtp.ssrcs.size(); |
| 231 | encoder_config.bitrate_priority = 1.0; |
| 232 | encoder_config.simulcast_layers.resize(encoder_config.number_of_streams); |
| 233 | for (size_t i = 0; i < encoder_config.number_of_streams; ++i) { |
| 234 | encoder_config.simulcast_layers[i].active = true; |
| 235 | encoder_config.simulcast_layers[i].min_bitrate_bps = kMinBitrateBps; |
| 236 | encoder_config.simulcast_layers[i].max_bitrate_bps = kMaxBitrateBps; |
| 237 | encoder_config.simulcast_layers[i].max_framerate = send_config.video_fps; |
| 238 | } |
| 239 | |
Benjamin Wright | 87bbb91 | 2019-02-01 18:00:05 | [diff] [blame] | 240 | // Setup the fake video stream for this. |
Ilya Nikolaevskiy | ca16021 | 2019-07-10 13:34:45 | [diff] [blame] | 241 | std::unique_ptr<test::FrameGeneratorCapturer> frame_generator = |
Mirko Bonadei | 317a1f0 | 2019-09-17 15:06:18 | [diff] [blame] | 242 | std::make_unique<test::FrameGeneratorCapturer>( |
Danil Chapovalov | 702820d | 2023-11-28 17:25:28 | [diff] [blame] | 243 | &env_.clock(), |
Artem Titov | 33f9d2b | 2019-12-05 14:59:00 | [diff] [blame] | 244 | test::CreateSquareFrameGenerator(send_config.video_width, |
| 245 | send_config.video_height, |
Florent Castelli | 8037fc6 | 2024-08-29 13:00:40 | [diff] [blame] | 246 | std::nullopt, std::nullopt), |
Danil Chapovalov | 702820d | 2023-11-28 17:25:28 | [diff] [blame] | 247 | send_config.video_fps, env_.task_queue_factory()); |
Benjamin Wright | 87bbb91 | 2019-02-01 18:00:05 | [diff] [blame] | 248 | frame_generator->Init(); |
| 249 | |
| 250 | VideoSendStream* video_send_stream = call_->CreateVideoSendStream( |
| 251 | std::move(video_config), std::move(encoder_config)); |
| 252 | video_send_stream->SetSource( |
| 253 | frame_generator.get(), |
| 254 | webrtc::DegradationPreference::MAINTAIN_FRAMERATE); |
| 255 | // Store these objects so we can destropy them at the end. |
| 256 | frame_generators_.push_back(std::move(frame_generator)); |
| 257 | video_send_streams_.push_back(video_send_stream); |
| 258 | } |
| 259 | } |
| 260 | |
| 261 | RtpGenerator::~RtpGenerator() { |
| 262 | for (VideoSendStream* send_stream : video_send_streams_) { |
| 263 | call_->DestroyVideoSendStream(send_stream); |
| 264 | } |
| 265 | } |
| 266 | |
| 267 | void RtpGenerator::GenerateRtpDump(const std::string& rtp_dump_path) { |
| 268 | rtp_dump_writer_.reset(test::RtpFileWriter::Create( |
| 269 | test::RtpFileWriter::kRtpDump, rtp_dump_path)); |
| 270 | |
| 271 | call_->SignalChannelNetworkState(webrtc::MediaType::VIDEO, |
| 272 | webrtc::kNetworkUp); |
| 273 | for (VideoSendStream* send_stream : video_send_streams_) { |
| 274 | send_stream->Start(); |
| 275 | } |
| 276 | |
| 277 | // Spinlock until all the durations end. |
| 278 | WaitUntilAllVideoStreamsFinish(); |
| 279 | |
| 280 | call_->SignalChannelNetworkState(webrtc::MediaType::VIDEO, |
| 281 | webrtc::kNetworkDown); |
| 282 | } |
| 283 | |
Harald Alvestrand | d43af91 | 2023-08-15 11:41:45 | [diff] [blame] | 284 | bool RtpGenerator::SendRtp(rtc::ArrayView<const uint8_t> packet, |
Benjamin Wright | 87bbb91 | 2019-02-01 18:00:05 | [diff] [blame] | 285 | const webrtc::PacketOptions& options) { |
Harald Alvestrand | d43af91 | 2023-08-15 11:41:45 | [diff] [blame] | 286 | test::RtpPacket rtp_packet = DataToRtpPacket(packet.data(), packet.size()); |
Benjamin Wright | 87bbb91 | 2019-02-01 18:00:05 | [diff] [blame] | 287 | rtp_dump_writer_->WritePacket(&rtp_packet); |
| 288 | return true; |
| 289 | } |
| 290 | |
Harald Alvestrand | d43af91 | 2023-08-15 11:41:45 | [diff] [blame] | 291 | bool RtpGenerator::SendRtcp(rtc::ArrayView<const uint8_t> packet) { |
| 292 | test::RtpPacket rtcp_packet = DataToRtpPacket(packet.data(), packet.size()); |
Benjamin Wright | 87bbb91 | 2019-02-01 18:00:05 | [diff] [blame] | 293 | rtp_dump_writer_->WritePacket(&rtcp_packet); |
| 294 | return true; |
| 295 | } |
| 296 | |
| 297 | int RtpGenerator::GetMaxDuration() const { |
| 298 | int max_end_ms = 0; |
| 299 | for (const auto& video_stream : options_.video_streams) { |
| 300 | max_end_ms = std::max(video_stream.duration_ms, max_end_ms); |
| 301 | } |
| 302 | return max_end_ms; |
| 303 | } |
| 304 | |
| 305 | void RtpGenerator::WaitUntilAllVideoStreamsFinish() { |
| 306 | // Find the maximum duration required by the streams. |
| 307 | start_ms_ = Clock::GetRealTimeClock()->TimeInMilliseconds(); |
| 308 | int64_t max_end_ms = start_ms_ + GetMaxDuration(); |
| 309 | |
| 310 | int64_t current_time = 0; |
| 311 | do { |
| 312 | int64_t min_wait_time = 0; |
| 313 | current_time = Clock::GetRealTimeClock()->TimeInMilliseconds(); |
| 314 | // Stop any streams that are no longer active. |
| 315 | for (size_t i = 0; i < options_.video_streams.size(); ++i) { |
| 316 | const int64_t end_ms = start_ms_ + options_.video_streams[i].duration_ms; |
| 317 | if (current_time > end_ms) { |
| 318 | video_send_streams_[i]->Stop(); |
| 319 | } else { |
| 320 | min_wait_time = std::min(min_wait_time, end_ms - current_time); |
| 321 | } |
| 322 | } |
| 323 | rtc::Thread::Current()->SleepMs(min_wait_time); |
| 324 | } while (current_time < max_end_ms); |
| 325 | } |
| 326 | |
| 327 | test::RtpPacket RtpGenerator::DataToRtpPacket(const uint8_t* packet, |
| 328 | size_t packet_len) { |
| 329 | webrtc::test::RtpPacket rtp_packet; |
| 330 | memcpy(rtp_packet.data, packet, packet_len); |
| 331 | rtp_packet.length = packet_len; |
| 332 | rtp_packet.original_length = packet_len; |
| 333 | rtp_packet.time_ms = |
| 334 | webrtc::Clock::GetRealTimeClock()->TimeInMilliseconds() - start_ms_; |
| 335 | return rtp_packet; |
| 336 | } |
| 337 | |
| 338 | } // namespace webrtc |