MediaCapabilitiesValidation.cpp (20748B)
1 /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ 2 /* vim: set ts=2 et sw=2 tw=80: */ 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 https://mozilla.org/MPL/2.0/. */ 6 #include "MediaCapabilitiesValidation.h" 7 8 #include <algorithm> 9 #include <array> 10 #include <cmath> 11 12 #include "MediaMIMETypes.h" 13 #include "mozilla/Assertions.h" 14 #include "mozilla/ErrorResult.h" 15 #include "mozilla/Logging.h" 16 #include "mozilla/Result.h" 17 #include "mozilla/Variant.h" 18 #include "mozilla/dom/MediaCapabilitiesBinding.h" 19 #include "mozilla/dom/Promise.h" 20 #include "nsReadableUtils.h" 21 22 extern mozilla::LazyLogModule sMediaCapabilitiesLog; 23 #define LOG(args) MOZ_LOG(sMediaCapabilitiesLog, LogLevel::Debug, args) 24 25 namespace mozilla::mediacaps { 26 using dom::AudioConfiguration; 27 using dom::MediaConfiguration; 28 using dom::MediaDecodingConfiguration; 29 using dom::MediaDecodingType; 30 using dom::MediaEncodingConfiguration; 31 using dom::MediaEncodingType; 32 using dom::MSG_INVALID_MEDIA_AUDIO_CONFIGURATION; 33 using dom::MSG_INVALID_MEDIA_VIDEO_CONFIGURATION; 34 using dom::MSG_MISSING_REQUIRED_DICTIONARY_MEMBER; 35 using dom::Promise; 36 using dom::VideoConfiguration; 37 38 static nsAutoCString GetMIMEDebugString(const MediaConfiguration& aConfig); 39 static bool IsContainerType(const MediaExtendedMIMEType& aMime); 40 static bool IsSingleCodecType(const MediaExtendedMIMEType& aMime); 41 42 // If encodingOrDecodingType is webrtc (MediaEncodingType) or webrtc 43 // (MediaDecodingType) and mimeType is not one that is used with RTP 44 // (as defined in the specifications of the corresponding RTP payload formats 45 // [IANA-MEDIA-TYPES] [RFC6838]), return unsupported. 46 // 47 // Unsupported: iLBC, iSAC (Chrome, Safari) 48 // https://developer.mozilla.org/en-US/docs/Web/Media/Guides/Formats/WebRTC_codecs 49 static const std::array kSingleWebRTCCodecTypes = { 50 // "audio/ilbc"_ns, "audio/isac"_ns, 51 "audio/g711-alaw"_ns, "audio/g711-mlaw"_ns, "audio/g722"_ns, 52 "audio/opus"_ns, "audio/pcma"_ns, "audio/pcmu"_ns, 53 "video/av1"_ns, "video/h264"_ns, "video/vp8"_ns, 54 "video/vp9"_ns, 55 }; 56 57 static const std::array kContainerTypes = {"video/mkv"_ns, "video/mp4"_ns, 58 "video/webm"_ns, "audio/ogg"_ns, 59 "audio/mp4"_ns, "audio/webm"_ns}; 60 61 // https://w3c.github.io/media-capabilities/#check-mime-type-support 62 ValidationResult CheckMIMETypeSupport(const MediaExtendedMIMEType& aMime, 63 const AVType& aAVType, 64 const MediaType& aMediaType) { 65 // Step 1: If encodingOrDecodingType is webrtc (MediaEncodingType) or 66 // webrtc (MediaDecodingType) and mimeType is not one that is used with 67 // RTP (as defined in the specifications of the corresponding RTP payload 68 // formats [IANA-MEDIA-TYPES] [RFC6838]), return unsupported. 69 // TODO bug 1825286 70 71 // Step 2: If colorGamut is present and is not valid for mimeType, return 72 // unsupported. 73 // TODO bug 1825286 74 return Ok(); 75 } 76 77 // Checks MIME type validity as per: 78 // https://w3c.github.io/media-capabilities/#check-mime-type-validity 79 // NOTE: Open issue, https://github.com/w3c/media-capabilities/issues/238 80 // "Do WebRTC encoding/decoding types have the single-codec restrictions?" 81 static ValidationResult CheckMIMETypeValidity( 82 const MediaExtendedMIMEType& aMime, const AVType& aAVType, 83 const MediaType& aMediaType) { 84 // Step 1: If the type of mimeType per [RFC9110] is neither 85 // media nor application, return false. 86 const MediaMIMEType& mimetype = aMime.Type(); 87 if (!mimetype.HasAudioMajorType() && !mimetype.HasVideoMajorType() && 88 !mimetype.HasApplicationMajorType()) { 89 ValidationResult err = 90 Err(aAVType == AVType::AUDIO ? ValidationError::InvalidAudioType 91 : ValidationError::InvalidVideoType); 92 LOG( 93 ("[Invalid MIME Validity #1, %s] Rejecting - not media, not " 94 "application %s", 95 EnumValueToString(err.unwrapErr()), aMime.OriginalString().get())); 96 return err; 97 } 98 99 // The following two steps don't appear to be explicitly defined in the spec 100 // but are required for some WPT passes and seem like they'd make the most 101 // sense to have here. The tests in question can be found here: 102 // https://searchfox.org/firefox-main/rev/cd639e07f74b203d72b0f4a2bea757ae9e10401a/testing/web-platform/tests/media-capabilities/decodingInfo.any.js#140-161 103 104 // Step 1a?: Test that decodingInfo rejects if the audio configuration 105 // contentType is of type video 106 if (aAVType == AVType::AUDIO && !aMime.Type().HasAudioMajorType()) { 107 ValidationResult err = Err(ValidationError::InvalidAudioType); 108 LOG(("[Invalid MIME Validity #1a?, %s] Rejecting '%s'", 109 EnumValueToString(err.unwrapErr()), aMime.OriginalString().get())); 110 return err; 111 } 112 113 // Step 1b?: Test that decodingInfo rejects if the video configuration 114 // contentType is of type audio 115 if (aAVType == AVType::VIDEO && !aMime.Type().HasVideoMajorType()) { 116 ValidationResult err = Err(ValidationError::InvalidVideoType); 117 LOG(("[Invalid MIME Validity #1b?, %s] Rejecting '%s'", 118 EnumValueToString(err.unwrapErr()), aMime.OriginalString().get())); 119 return err; 120 } 121 122 // Step 2: If the combined type and subtype members of mimeType allow a 123 // single media codec and the parameters member of mimeType is not 124 // empty, return false. 125 // 126 // (NOTE: WEBRTC EXCEPTION, SEE ISSUE) 127 // https://github.com/w3c/media-capabilities/issues/238 128 // TODO bug 1825286 (WebRTC) 129 const size_t numParams = aMime.GetParameterCount(); 130 if (IsSingleCodecType(aMime) && numParams != 0) { 131 ValidationResult err = Err(ValidationError::SingleCodecHasParams); 132 LOG(("[Invalid MIME Validity #2, %s] Rejecting '%s'", 133 EnumValueToString(err.unwrapErr()), aMime.OriginalString().get())); 134 return err; 135 } 136 137 // Step 3: If the combined type and subtype members of mimeType allow 138 // multiple media codecs, run the following steps: 139 if (IsContainerType(aMime)) { 140 // Step 3.1: If the parameters member of mimeType does not contain a single 141 // key named "codecs", return false. 142 if ((numParams != 1) || !aMime.HaveCodecs()) { 143 ValidationResult err = Err(ValidationError::ContainerMissingCodecsParam); 144 LOG(("[Invalid MIME Validity #3.1, %s] Rejecting '%s'", 145 EnumValueToString(err.unwrapErr()), aMime.OriginalString().get())); 146 return err; 147 } 148 149 // Step 3.2: If the value of mimeType.parameters["codecs"] does not 150 // describe a single media codec, return false. 151 const auto& codecs = aMime.Codecs(); 152 if (!aMime.HaveCodecs() || codecs.IsEmpty() || 153 codecs.AsString().FindChar(',') != kNotFound) { 154 ValidationResult err = Err(ValidationError::ContainerCodecsNotSingle); 155 LOG(("[Invalid MIME #3.2, %s] Rejecting '%s'", 156 EnumValueToString(err.unwrapErr()), aMime.OriginalString().get())); 157 return err; 158 } 159 } 160 161 // Step 4: Return true 162 return Ok(); 163 } 164 165 // https://w3c.github.io/media-capabilities/#audioconfiguration 166 ValidationResult IsValidAudioConfiguration(const AudioConfiguration& aConfig, 167 const MediaType& aType) { 168 // Step 1: Let mimeType be the result of running parse a MIME type with 169 // configuration’s contentType. 170 const Maybe<MediaExtendedMIMEType> mime = 171 MakeMediaExtendedMIMEType(aConfig.mContentType); 172 173 // Step 2: If mimeType is failure, return false. 174 if (!mime) { 175 ValidationResult err = Err(ValidationError::InvalidAudioType); 176 LOG(("[Invalid AudioConfiguration #2, %s] Rejecting '%s'\n", 177 EnumValueToString(err.unwrapErr()), 178 NS_ConvertUTF16toUTF8(aConfig.mContentType).get())); 179 return err; 180 } 181 182 // Return the result of running check MIME type validity with mimeType and 183 // audio. The channels member represents the audio channels used by the audio 184 // track. channels is only applicable to the decoding types media-source, 185 // file, and webrtc and the encoding type webrtc. 186 return CheckMIMETypeValidity(mime.ref(), AVType::AUDIO, aType); 187 } 188 189 // https://w3c.github.io/media-capabilities/#audioconfiguration 190 // To check if a VideoConfiguration configuration is a valid video 191 // configuration, the following steps MUST be run... 192 template <typename CodingType> 193 ValidationResult IsValidVideoConfiguration(const VideoConfiguration& aConfig, 194 const CodingType& aType) { 195 static_assert(std::is_same_v<std::decay_t<CodingType>, MediaEncodingType> || 196 std::is_same_v<CodingType, MediaDecodingType>, 197 "tType must be MediaEncodingType or MediaDecodingType"); 198 199 // Step 1: If framerate is not finite or is not greater than 0, 200 // return false and abort these steps. 201 if (!isfinite(aConfig.mFramerate) || !(aConfig.mFramerate > 0)) { 202 ValidationResult err = Err(ValidationError::FramerateInvalid); 203 LOG(("[Invalid VideoConfiguration (Framerate, %s) #1] Rejecting '%s'\n", 204 EnumValueToString(err.unwrapErr()), 205 NS_ConvertUTF16toUTF8(aConfig.mContentType).get())); 206 return err; 207 } 208 209 // Step 2: If an optional member is specified for a MediaDecodingType or 210 // MediaEncodingType to which it’s not applicable, return false and abort 211 // these steps. See applicability rules in the member definitions below. 212 if constexpr (std::is_same_v<CodingType, MediaDecodingType>) { 213 // hdrMetadataType is only applicable to MediaDecodingConfiguration 214 // for types media-source and file. 215 if (aConfig.mHdrMetadataType.WasPassed() && 216 aType != MediaDecodingType::File && 217 aType != MediaDecodingType::Media_source) { 218 ValidationResult err = Err(ValidationError::InapplicableMember); 219 LOG(("[Invalid VideoConfiguration (HDR, %s) #2] Rejecting '%s'\n", 220 EnumValueToString(err.unwrapErr()), 221 NS_ConvertUTF16toUTF8(aConfig.mContentType).get())); 222 return err; 223 } 224 // colorGamut is only applicable to 225 // MediaDecodingConfiguration for types media-source and file. 226 if (aConfig.mColorGamut.WasPassed() && aType != MediaDecodingType::File && 227 aType != MediaDecodingType::Media_source) { 228 ValidationResult err = Err(ValidationError::InapplicableMember); 229 LOG(("[Invalid VideoConfiguration (Color Gamut, %s) #2] Rejecting '%s'\n", 230 EnumValueToString(err.unwrapErr()), 231 NS_ConvertUTF16toUTF8(aConfig.mContentType).get())); 232 return err; 233 } 234 235 // transferFunction is only 236 // applicable to MediaDecodingConfiguration for types media-source and file. 237 if (aConfig.mTransferFunction.WasPassed() && 238 aType != MediaDecodingType::File && 239 aType != MediaDecodingType::Media_source) { 240 ValidationResult err = Err(ValidationError::InapplicableMember); 241 LOG( 242 ("[Invalid VideoConfiguration (Transfer Function, %s) #2] Rejecting " 243 "'%s'\n", 244 EnumValueToString(err.unwrapErr()), 245 NS_ConvertUTF16toUTF8(aConfig.mContentType).get())); 246 return err; 247 } 248 } 249 250 // ScalabilityMode is only applicable to MediaEncodingConfiguration 251 // for type webrtc. 252 // TODO bug 1825286 253 254 // Step 3: Let mimeType be the result of running parse a MIME type with 255 // configuration’s contentType. 256 const Maybe<MediaExtendedMIMEType> mime = 257 MakeMediaExtendedMIMEType(aConfig.mContentType); 258 259 // Step 4: If mimeType is failure, return false. 260 if (!mime) { 261 ValidationResult err = Err(ValidationError::InvalidVideoType); 262 LOG(("[Invalid VideoConfiguration (MIME failure, %s) #4] Rejecting '%s'\n", 263 EnumValueToString(err.unwrapErr()), 264 NS_ConvertUTF16toUTF8(aConfig.mContentType).get())); 265 return err; 266 } 267 268 // Step 5: Return the result of running check MIME type validity 269 // with mimeType and video. 270 return CheckMIMETypeValidity(mime.ref(), AVType::VIDEO, AsVariant(aType)); 271 } 272 273 template ValidationResult IsValidVideoConfiguration<MediaEncodingType>( 274 const VideoConfiguration&, const MediaEncodingType&); 275 template ValidationResult IsValidVideoConfiguration<MediaDecodingType>( 276 const VideoConfiguration&, const MediaDecodingType&); 277 278 ValidationResult IsValidVideoConfiguration(const VideoConfiguration& aConfig, 279 const MediaType& aType) { 280 return aType.match( 281 [&](const MediaEncodingType& t) { 282 return IsValidVideoConfiguration(aConfig, t); 283 }, 284 [&](const MediaDecodingType& t) { 285 return IsValidVideoConfiguration(aConfig, t); 286 }); 287 } 288 289 // https://w3c.github.io/media-capabilities/#mediaconfiguration 290 ValidationResult IsValidMediaConfiguration(const MediaConfiguration& aConfig, 291 const MediaType& aType) { 292 // Step 1: audio and/or video MUST exist. 293 if (!aConfig.mVideo.WasPassed() && !aConfig.mAudio.WasPassed()) { 294 ValidationResult err = Err(ValidationError::MissingType); 295 LOG(("[Invalid Media Configuration (No A/V, %s) #1] '%s'", 296 EnumValueToString(err.unwrapErr()), 297 GetMIMEDebugString(aConfig).get())); 298 return err; 299 } 300 301 // Step 2: audio MUST be a valid audio configuration if it exists. 302 if (aConfig.mAudio.WasPassed()) { 303 auto rv = IsValidAudioConfiguration(aConfig.mAudio.Value(), aType); 304 if (rv.isErr()) { 305 LOG(("[Invalid Media Configuration (Invalid Audio, %s) #2] '%s'", 306 EnumValueToString(rv.unwrapErr()), 307 GetMIMEDebugString(aConfig).get())); 308 return rv; 309 } 310 } 311 312 // Step 3: video MUST be a valid video configuration if it exists. 313 if (aConfig.mVideo.WasPassed()) { 314 auto rv = IsValidVideoConfiguration(aConfig.mVideo.Value(), aType); 315 if (rv.isErr()) { 316 LOG(("[Invalid Media Configuration (Invalid Video, %s) #3] '%s'", 317 EnumValueToString(rv.unwrapErr()), 318 GetMIMEDebugString(aConfig).get())); 319 return rv; 320 } 321 } 322 return Ok(); 323 } 324 325 // No specific validation steps in the spec... 326 ValidationResult IsValidMediaEncodingConfiguration( 327 const MediaEncodingConfiguration& aConfig) { 328 return IsValidMediaConfiguration(aConfig, AsVariant(aConfig.mType)); 329 } 330 331 // https://w3c.github.io/media-capabilities/#mediaconfiguration 332 ValidationResult IsValidMediaDecodingConfiguration( 333 const MediaDecodingConfiguration& aConfig) { 334 // For a MediaDecodingConfiguration to be a valid MediaDecodingConfiguration, 335 // all of the following conditions MUST be true: 336 337 // Step 1: It MUST be a valid MediaConfiguration. 338 auto base = IsValidMediaConfiguration(aConfig, AsVariant(aConfig.mType)); 339 if (base.isErr()) { 340 LOG( 341 ("[Invalid MediaDecodingConfiguration (Invalid MediaConfiguration, %s) " 342 "#1]", 343 EnumValueToString(base.unwrapErr()))); 344 return base; 345 } 346 // Step 2: If keySystemConfiguration exists... 347 if (aConfig.mKeySystemConfiguration.WasPassed()) { 348 const auto& keySystemConfig = aConfig.mKeySystemConfiguration.Value(); 349 350 // Step 2.1: The type MUST be media-source or file. 351 if (aConfig.mType != MediaDecodingType::File && 352 aConfig.mType != MediaDecodingType::Media_source) { 353 ValidationResult err = Err(ValidationError::KeySystemWrongType); 354 LOG(("[Invalid MediaDecodingConfiguration (keysystem, %s) #2.1]", 355 EnumValueToString(err.unwrapErr()))); 356 return err; 357 } 358 359 // Step 2.2: If keySystemConfiguration.audio exists, audio MUST also exist. 360 if (keySystemConfig.mAudio.WasPassed() && !aConfig.mAudio.WasPassed()) { 361 ValidationResult err = Err(ValidationError::KeySystemAudioMissing); 362 LOG(("[Invalid MediaDecodingConfiguration (keysystem, %s) #2.2]", 363 EnumValueToString(err.unwrapErr()))); 364 return err; 365 } 366 367 // Step 2.3: If keySystemConfiguration.video exists, video MUST also exist. 368 if (keySystemConfig.mVideo.WasPassed() && !aConfig.mVideo.WasPassed()) { 369 ValidationResult err = Err(ValidationError::KeySystemVideoMissing); 370 LOG(("[Invalid MediaDecodingConfiguration (keysystem, %s) #2.3]", 371 EnumValueToString(err.unwrapErr()))); 372 return err; 373 } 374 } 375 return Ok(); 376 } 377 378 ///////////////////////////////// 379 // Helper functions begin here // 380 ///////////////////////////////// 381 382 void RejectWithValidationResult(Promise* aPromise, const ValidationError aErr) { 383 switch (aErr) { 384 case ValidationError::MissingType: 385 aPromise->MaybeRejectWithTypeError( 386 "'audio' or 'video' member of argument of MediaCapabilities"); 387 return; 388 case ValidationError::InvalidAudioConfiguration: 389 aPromise->MaybeRejectWithTypeError("Invalid AudioConfiguration!"); 390 return; 391 case ValidationError::InvalidAudioType: 392 aPromise->MaybeRejectWithTypeError( 393 "Invalid AudioConfiguration MIME type"); 394 return; 395 case ValidationError::InvalidVideoConfiguration: 396 aPromise->MaybeRejectWithTypeError("Invalid VideoConfiguration!"); 397 return; 398 case ValidationError::InvalidVideoType: 399 aPromise->MaybeRejectWithTypeError("Invalid Video MIME type"); 400 return; 401 case ValidationError::SingleCodecHasParams: 402 aPromise->MaybeRejectWithTypeError("Single codec has parameters"); 403 return; 404 case ValidationError::ContainerMissingCodecsParam: 405 aPromise->MaybeRejectWithTypeError("Container missing codec parameters"); 406 return; 407 case ValidationError::ContainerCodecsNotSingle: 408 aPromise->MaybeRejectWithTypeError("Container has more than one codec"); 409 return; 410 case ValidationError::FramerateInvalid: 411 aPromise->MaybeRejectWithTypeError("Invalid frame rate"); 412 return; 413 case ValidationError::InapplicableMember: 414 aPromise->MaybeRejectWithTypeError("Inapplicable member"); 415 return; 416 case ValidationError::KeySystemWrongType: 417 case ValidationError::KeySystemAudioMissing: 418 case ValidationError::KeySystemVideoMissing: 419 aPromise->MaybeRejectWithTypeError("Invalid keysystem configuration"); 420 return; 421 default: 422 MOZ_ASSERT_UNREACHABLE("Unhandled MediaCapabilities validation error!"); 423 return; 424 } 425 } 426 427 void ThrowWithValidationResult(ErrorResult& aRv, const ValidationError aErr) { 428 switch (aErr) { 429 case ValidationError::MissingType: 430 aRv.ThrowTypeError<MSG_MISSING_REQUIRED_DICTIONARY_MEMBER>( 431 "'audio' or 'video' member of argument of MediaCapabilities"); 432 return; 433 case ValidationError::InvalidAudioConfiguration: 434 aRv.ThrowTypeError<MSG_INVALID_MEDIA_AUDIO_CONFIGURATION>(); 435 return; 436 case ValidationError::InvalidAudioType: 437 case ValidationError::KeySystemAudioMissing: 438 aRv.ThrowTypeError<MSG_INVALID_MEDIA_AUDIO_CONFIGURATION>(); 439 return; 440 case ValidationError::InvalidVideoConfiguration: 441 case ValidationError::InvalidVideoType: 442 case ValidationError::SingleCodecHasParams: 443 case ValidationError::ContainerMissingCodecsParam: 444 case ValidationError::ContainerCodecsNotSingle: 445 case ValidationError::FramerateInvalid: 446 case ValidationError::InapplicableMember: 447 aRv.ThrowTypeError<MSG_INVALID_MEDIA_VIDEO_CONFIGURATION>(); 448 return; 449 case ValidationError::KeySystemWrongType: 450 case ValidationError::KeySystemVideoMissing: 451 aRv.ThrowTypeError<MSG_INVALID_MEDIA_VIDEO_CONFIGURATION>(); 452 return; 453 default: 454 MOZ_ASSERT_UNREACHABLE("Unhandled MediaCapabilities validation error!"); 455 return; 456 } 457 } 458 459 template <size_t N> 460 static bool MimePrefixStartsWith( 461 const MediaExtendedMIMEType& aMime, 462 const std::array<nsLiteralCString, N>& aPrefixes) { 463 const nsACString& s = aMime.OriginalString(); 464 return std::any_of(aPrefixes.begin(), aPrefixes.end(), [&](const auto& p) { 465 return StringBeginsWith(s, p, nsCaseInsensitiveCStringComparator); 466 }); 467 } 468 static bool IsContainerType(const MediaExtendedMIMEType& aMime) { 469 return MimePrefixStartsWith(aMime, kContainerTypes); 470 } 471 static bool IsSingleCodecType(const MediaExtendedMIMEType& aMime) { 472 return MimePrefixStartsWith(aMime, kSingleWebRTCCodecTypes); 473 } 474 475 static nsAutoCString GetMIMEDebugString(const MediaConfiguration& aConfig) { 476 nsAutoCString result; 477 result.SetCapacity(64); 478 result.AssignLiteral("Audio MIME: "); 479 if (aConfig.mAudio.WasPassed()) { 480 result.Append(NS_ConvertUTF16toUTF8(aConfig.mAudio.Value().mContentType)); 481 } else { 482 result.AppendLiteral("(none)"); 483 } 484 result.AppendLiteral(" Video MIME: "); 485 if (aConfig.mVideo.WasPassed()) { 486 result.Append(NS_ConvertUTF16toUTF8(aConfig.mVideo.Value().mContentType)); 487 } else { 488 result.AppendLiteral("(none)"); 489 } 490 return result; 491 } 492 493 } // namespace mozilla::mediacaps 494 #undef LOG